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 17package com.android.inputmethod.keyboard.internal; 18 19import android.content.res.Resources; 20import android.text.TextUtils; 21import android.util.Log; 22 23import com.android.inputmethod.keyboard.Keyboard; 24import com.android.inputmethod.latin.R; 25 26import java.util.ArrayList; 27 28/** 29 * String parser of moreKeys attribute of Key. 30 * The string is comma separated texts each of which represents one "more key". 31 * Each "more key" specification is one of the following: 32 * - A single letter (Letter) 33 * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText). 34 * - Icon followed by keyOutputText or code (@icon/icon_number|@integer/key_code) 35 * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' 36 * character. 37 * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well. 38 * See {@link KeyboardIconsSet} about icon_number. 39 */ 40public class MoreKeySpecParser { 41 private static final String TAG = MoreKeySpecParser.class.getSimpleName(); 42 43 private static final char ESCAPE = '\\'; 44 private static final String LABEL_END = "|"; 45 private static final String PREFIX_AT = "@"; 46 private static final String PREFIX_ICON = PREFIX_AT + "icon/"; 47 private static final String PREFIX_CODE = PREFIX_AT + "integer/"; 48 49 private MoreKeySpecParser() { 50 // Intentional empty constructor for utility class. 51 } 52 53 private static boolean hasIcon(String moreKeySpec) { 54 if (moreKeySpec.startsWith(PREFIX_ICON)) { 55 final int end = indexOfLabelEnd(moreKeySpec, 0); 56 if (end > 0) 57 return true; 58 throw new MoreKeySpecParserError("outputText or code not specified: " + moreKeySpec); 59 } 60 return false; 61 } 62 63 private static boolean hasCode(String moreKeySpec) { 64 final int end = indexOfLabelEnd(moreKeySpec, 0); 65 if (end > 0 && end + 1 < moreKeySpec.length() 66 && moreKeySpec.substring(end + 1).startsWith(PREFIX_CODE)) { 67 return true; 68 } 69 return false; 70 } 71 72 private static String parseEscape(String text) { 73 if (text.indexOf(ESCAPE) < 0) 74 return text; 75 final int length = text.length(); 76 final StringBuilder sb = new StringBuilder(); 77 for (int pos = 0; pos < length; pos++) { 78 final char c = text.charAt(pos); 79 if (c == ESCAPE && pos + 1 < length) { 80 sb.append(text.charAt(++pos)); 81 } else { 82 sb.append(c); 83 } 84 } 85 return sb.toString(); 86 } 87 88 private static int indexOfLabelEnd(String moreKeySpec, int start) { 89 if (moreKeySpec.indexOf(ESCAPE, start) < 0) { 90 final int end = moreKeySpec.indexOf(LABEL_END, start); 91 if (end == 0) 92 throw new MoreKeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec); 93 return end; 94 } 95 final int length = moreKeySpec.length(); 96 for (int pos = start; pos < length; pos++) { 97 final char c = moreKeySpec.charAt(pos); 98 if (c == ESCAPE && pos + 1 < length) { 99 pos++; 100 } else if (moreKeySpec.startsWith(LABEL_END, pos)) { 101 return pos; 102 } 103 } 104 return -1; 105 } 106 107 public static String getLabel(String moreKeySpec) { 108 if (hasIcon(moreKeySpec)) 109 return null; 110 final int end = indexOfLabelEnd(moreKeySpec, 0); 111 final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) 112 : parseEscape(moreKeySpec); 113 if (TextUtils.isEmpty(label)) 114 throw new MoreKeySpecParserError("Empty label: " + moreKeySpec); 115 return label; 116 } 117 118 public static String getOutputText(String moreKeySpec) { 119 if (hasCode(moreKeySpec)) 120 return null; 121 final int end = indexOfLabelEnd(moreKeySpec, 0); 122 if (end > 0) { 123 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) 124 throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": " 125 + moreKeySpec); 126 final String outputText = parseEscape(moreKeySpec.substring(end + LABEL_END.length())); 127 if (!TextUtils.isEmpty(outputText)) 128 return outputText; 129 throw new MoreKeySpecParserError("Empty outputText: " + moreKeySpec); 130 } 131 final String label = getLabel(moreKeySpec); 132 if (label == null) 133 throw new MoreKeySpecParserError("Empty label: " + moreKeySpec); 134 // Code is automatically generated for one letter label. See {@link getCode()}. 135 if (label.length() == 1) 136 return null; 137 return label; 138 } 139 140 public static int getCode(Resources res, String moreKeySpec) { 141 if (hasCode(moreKeySpec)) { 142 final int end = indexOfLabelEnd(moreKeySpec, 0); 143 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) 144 throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); 145 final int resId = getResourceId(res, 146 moreKeySpec.substring(end + LABEL_END.length() + PREFIX_AT.length())); 147 final int code = res.getInteger(resId); 148 return code; 149 } 150 if (indexOfLabelEnd(moreKeySpec, 0) > 0) 151 return Keyboard.CODE_DUMMY; 152 final String label = getLabel(moreKeySpec); 153 // Code is automatically generated for one letter label. 154 if (label != null && label.length() == 1) 155 return label.charAt(0); 156 return Keyboard.CODE_DUMMY; 157 } 158 159 public static int getIconId(String moreKeySpec) { 160 if (hasIcon(moreKeySpec)) { 161 int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length() + 1); 162 final String iconId = moreKeySpec.substring(PREFIX_ICON.length(), end); 163 try { 164 return Integer.valueOf(iconId); 165 } catch (NumberFormatException e) { 166 Log.w(TAG, "illegal icon id specified: " + iconId); 167 return KeyboardIconsSet.ICON_UNDEFINED; 168 } 169 } 170 return KeyboardIconsSet.ICON_UNDEFINED; 171 } 172 173 private static int getResourceId(Resources res, String name) { 174 String packageName = res.getResourcePackageName(R.string.english_ime_name); 175 int resId = res.getIdentifier(name, null, packageName); 176 if (resId == 0) 177 throw new MoreKeySpecParserError("Unknown resource: " + name); 178 return resId; 179 } 180 181 @SuppressWarnings("serial") 182 public static class MoreKeySpecParserError extends RuntimeException { 183 public MoreKeySpecParserError(String message) { 184 super(message); 185 } 186 } 187 188 public interface CodeFilter { 189 public boolean shouldFilterOut(int code); 190 } 191 192 public static final CodeFilter DIGIT_FILTER = new CodeFilter() { 193 @Override 194 public boolean shouldFilterOut(int code) { 195 return Character.isDigit(code); 196 } 197 }; 198 199 public static CharSequence[] filterOut(Resources res, CharSequence[] moreKeys, 200 CodeFilter filter) { 201 if (moreKeys == null || moreKeys.length < 1) { 202 return null; 203 } 204 if (moreKeys.length == 1 205 && filter.shouldFilterOut(getCode(res, moreKeys[0].toString()))) { 206 return null; 207 } 208 ArrayList<CharSequence> filtered = null; 209 for (int i = 0; i < moreKeys.length; i++) { 210 final CharSequence moreKeySpec = moreKeys[i]; 211 if (filter.shouldFilterOut(getCode(res, moreKeySpec.toString()))) { 212 if (filtered == null) { 213 filtered = new ArrayList<CharSequence>(); 214 for (int j = 0; j < i; j++) { 215 filtered.add(moreKeys[j]); 216 } 217 } 218 } else if (filtered != null) { 219 filtered.add(moreKeySpec); 220 } 221 } 222 if (filtered == null) { 223 return moreKeys; 224 } 225 if (filtered.size() == 0) { 226 return null; 227 } 228 return filtered.toArray(new CharSequence[filtered.size()]); 229 } 230} 231