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