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