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.latin.utils; 18 19 import android.content.res.Resources; 20 import android.content.res.TypedArray; 21 import android.os.Build; 22 import android.text.TextUtils; 23 import android.util.DisplayMetrics; 24 import android.util.Log; 25 import android.util.TypedValue; 26 27 import com.android.inputmethod.annotations.UsedForTesting; 28 import com.android.inputmethod.latin.R; 29 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 import java.util.regex.PatternSyntaxException; 33 34 public final class ResourceUtils { 35 private static final String TAG = ResourceUtils.class.getSimpleName(); 36 37 public static final float UNDEFINED_RATIO = -1.0f; 38 public static final int UNDEFINED_DIMENSION = -1; 39 40 private ResourceUtils() { 41 // This utility class is not publicly instantiable. 42 } 43 44 private static final HashMap<String, String> sDeviceOverrideValueMap = 45 CollectionUtils.newHashMap(); 46 47 private static final String[] BUILD_KEYS_AND_VALUES = { 48 "HARDWARE", Build.HARDWARE, 49 "MODEL", Build.MODEL, 50 "BRAND", Build.BRAND, 51 "MANUFACTURER", Build.MANUFACTURER 52 }; 53 private static final HashMap<String, String> sBuildKeyValues; 54 private static final String sBuildKeyValuesDebugString; 55 56 static { 57 sBuildKeyValues = CollectionUtils.newHashMap(); 58 final ArrayList<String> keyValuePairs = CollectionUtils.newArrayList(); 59 final int keyCount = BUILD_KEYS_AND_VALUES.length / 2; 60 for (int i = 0; i < keyCount; i++) { 61 final int index = i * 2; 62 final String key = BUILD_KEYS_AND_VALUES[index]; 63 final String value = BUILD_KEYS_AND_VALUES[index + 1]; 64 sBuildKeyValues.put(key, value); 65 keyValuePairs.add(key + '=' + value); 66 } 67 sBuildKeyValuesDebugString = "[" + TextUtils.join(" ", keyValuePairs) + "]"; 68 } 69 70 public static String getDeviceOverrideValue(final Resources res, final int overrideResId) { 71 final int orientation = res.getConfiguration().orientation; 72 final String key = overrideResId + "-" + orientation; 73 if (sDeviceOverrideValueMap.containsKey(key)) { 74 return sDeviceOverrideValueMap.get(key); 75 } 76 77 final String[] overrideArray = res.getStringArray(overrideResId); 78 final String overrideValue = findConstantForKeyValuePairs(sBuildKeyValues, overrideArray); 79 // The overrideValue might be an empty string. 80 if (overrideValue != null) { 81 Log.i(TAG, "Find override value:" 82 + " resource="+ res.getResourceEntryName(overrideResId) 83 + " build=" + sBuildKeyValuesDebugString 84 + " override=" + overrideValue); 85 sDeviceOverrideValueMap.put(key, overrideValue); 86 return overrideValue; 87 } 88 89 String defaultValue = null; 90 try { 91 defaultValue = findDefaultConstant(overrideArray); 92 // The defaultValue might be an empty string. 93 if (defaultValue == null) { 94 Log.w(TAG, "Couldn't find override value nor default value:" 95 + " resource="+ res.getResourceEntryName(overrideResId) 96 + " build=" + sBuildKeyValuesDebugString); 97 } else { 98 Log.i(TAG, "Found default value:" 99 + " resource="+ res.getResourceEntryName(overrideResId) 100 + " build=" + sBuildKeyValuesDebugString 101 + " default=" + defaultValue); 102 } 103 } catch (final DeviceOverridePatternSyntaxError e) { 104 Log.w(TAG, "Syntax error, ignored", e); 105 } 106 sDeviceOverrideValueMap.put(key, defaultValue); 107 return defaultValue; 108 } 109 110 @SuppressWarnings("serial") 111 static class DeviceOverridePatternSyntaxError extends Exception { 112 public DeviceOverridePatternSyntaxError(final String message, final String expression) { 113 this(message, expression, null); 114 } 115 116 public DeviceOverridePatternSyntaxError(final String message, final String expression, 117 final Throwable throwable) { 118 super(message + ": " + expression, throwable); 119 } 120 } 121 122 /** 123 * Find the condition that fulfills specified key value pairs from an array of 124 * "condition,constant", and return the corresponding string constant. A condition is 125 * "pattern1[:pattern2...] (or an empty string for the default). A pattern is 126 * "key=regexp_value" string. The condition matches only if all patterns of the condition 127 * are true for the specified key value pairs. 128 * 129 * For example, "condition,constant" has the following format. 130 * (See {@link ResourceUtilsTests#testFindConstantForKeyValuePairsRegexp()}) 131 * - HARDWARE=mako,constantForNexus4 132 * - MODEL=Nexus 4:MANUFACTURER=LGE,constantForNexus4 133 * - ,defaultConstant 134 * 135 * @param keyValuePairs attributes to be used to look for a matched condition. 136 * @param conditionConstantArray an array of "condition,constant" elements to be searched. 137 * @return the constant part of the matched "condition,constant" element. Returns null if no 138 * condition matches. 139 */ 140 @UsedForTesting 141 static String findConstantForKeyValuePairs(final HashMap<String, String> keyValuePairs, 142 final String[] conditionConstantArray) { 143 if (conditionConstantArray == null || keyValuePairs == null) { 144 return null; 145 } 146 String foundValue = null; 147 for (final String conditionConstant : conditionConstantArray) { 148 final int posComma = conditionConstant.indexOf(','); 149 if (posComma < 0) { 150 Log.w(TAG, "Array element has no comma: " + conditionConstant); 151 continue; 152 } 153 final String condition = conditionConstant.substring(0, posComma); 154 if (condition.isEmpty()) { 155 // Default condition. The default condition should be searched by 156 // {@link #findConstantForDefault(String[])}. 157 continue; 158 } 159 try { 160 if (fulfillsCondition(keyValuePairs, condition)) { 161 // Take first match 162 if (foundValue == null) { 163 foundValue = conditionConstant.substring(posComma + 1); 164 } 165 // And continue walking through all conditions. 166 } 167 } catch (final DeviceOverridePatternSyntaxError e) { 168 Log.w(TAG, "Syntax error, ignored", e); 169 } 170 } 171 return foundValue; 172 } 173 174 private static boolean fulfillsCondition(final HashMap<String,String> keyValuePairs, 175 final String condition) throws DeviceOverridePatternSyntaxError { 176 final String[] patterns = condition.split(":"); 177 // Check all patterns in a condition are true 178 boolean matchedAll = true; 179 for (final String pattern : patterns) { 180 final int posEqual = pattern.indexOf('='); 181 if (posEqual < 0) { 182 throw new DeviceOverridePatternSyntaxError("Pattern has no '='", condition); 183 } 184 final String key = pattern.substring(0, posEqual); 185 final String value = keyValuePairs.get(key); 186 if (value == null) { 187 throw new DeviceOverridePatternSyntaxError("Unknown key", condition); 188 } 189 final String patternRegexpValue = pattern.substring(posEqual + 1); 190 try { 191 if (!value.matches(patternRegexpValue)) { 192 matchedAll = false; 193 // And continue walking through all patterns. 194 } 195 } catch (final PatternSyntaxException e) { 196 throw new DeviceOverridePatternSyntaxError("Syntax error", condition, e); 197 } 198 } 199 return matchedAll; 200 } 201 202 @UsedForTesting 203 static String findDefaultConstant(final String[] conditionConstantArray) 204 throws DeviceOverridePatternSyntaxError { 205 if (conditionConstantArray == null) { 206 return null; 207 } 208 for (final String condition : conditionConstantArray) { 209 final int posComma = condition.indexOf(','); 210 if (posComma < 0) { 211 throw new DeviceOverridePatternSyntaxError("Array element has no comma", condition); 212 } 213 if (posComma == 0) { // condition is empty. 214 return condition.substring(posComma + 1); 215 } 216 } 217 return null; 218 } 219 220 public static int getDefaultKeyboardWidth(final Resources res) { 221 final DisplayMetrics dm = res.getDisplayMetrics(); 222 return dm.widthPixels; 223 } 224 225 public static int getDefaultKeyboardHeight(final Resources res) { 226 final DisplayMetrics dm = res.getDisplayMetrics(); 227 final String keyboardHeightString = getDeviceOverrideValue(res, R.array.keyboard_heights); 228 final float keyboardHeight; 229 if (TextUtils.isEmpty(keyboardHeightString)) { 230 keyboardHeight = res.getDimension(R.dimen.keyboardHeight); 231 } else { 232 keyboardHeight = Float.parseFloat(keyboardHeightString) * dm.density; 233 } 234 final float maxKeyboardHeight = res.getFraction( 235 R.fraction.maxKeyboardHeight, dm.heightPixels, dm.heightPixels); 236 float minKeyboardHeight = res.getFraction( 237 R.fraction.minKeyboardHeight, dm.heightPixels, dm.heightPixels); 238 if (minKeyboardHeight < 0.0f) { 239 // Specified fraction was negative, so it should be calculated against display 240 // width. 241 minKeyboardHeight = -res.getFraction( 242 R.fraction.minKeyboardHeight, dm.widthPixels, dm.widthPixels); 243 } 244 // Keyboard height will not exceed maxKeyboardHeight and will not be less than 245 // minKeyboardHeight. 246 return (int)Math.max(Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight); 247 } 248 249 public static boolean isValidFraction(final float fraction) { 250 return fraction >= 0.0f; 251 } 252 253 // {@link Resources#getDimensionPixelSize(int)} returns at least one pixel size. 254 public static boolean isValidDimensionPixelSize(final int dimension) { 255 return dimension > 0; 256 } 257 258 // {@link Resources#getDimensionPixelOffset(int)} may return zero pixel offset. 259 public static boolean isValidDimensionPixelOffset(final int dimension) { 260 return dimension >= 0; 261 } 262 263 public static float getFraction(final TypedArray a, final int index, final float defValue) { 264 final TypedValue value = a.peekValue(index); 265 if (value == null || !isFractionValue(value)) { 266 return defValue; 267 } 268 return a.getFraction(index, 1, 1, defValue); 269 } 270 271 public static float getFraction(final TypedArray a, final int index) { 272 return getFraction(a, index, UNDEFINED_RATIO); 273 } 274 275 public static int getDimensionPixelSize(final TypedArray a, final int index) { 276 final TypedValue value = a.peekValue(index); 277 if (value == null || !isDimensionValue(value)) { 278 return ResourceUtils.UNDEFINED_DIMENSION; 279 } 280 return a.getDimensionPixelSize(index, ResourceUtils.UNDEFINED_DIMENSION); 281 } 282 283 public static float getDimensionOrFraction(final TypedArray a, final int index, final int base, 284 final float defValue) { 285 final TypedValue value = a.peekValue(index); 286 if (value == null) { 287 return defValue; 288 } 289 if (isFractionValue(value)) { 290 return a.getFraction(index, base, base, defValue); 291 } else if (isDimensionValue(value)) { 292 return a.getDimension(index, defValue); 293 } 294 return defValue; 295 } 296 297 public static int getEnumValue(final TypedArray a, final int index, final int defValue) { 298 final TypedValue value = a.peekValue(index); 299 if (value == null) { 300 return defValue; 301 } 302 if (isIntegerValue(value)) { 303 return a.getInt(index, defValue); 304 } 305 return defValue; 306 } 307 308 public static boolean isFractionValue(final TypedValue v) { 309 return v.type == TypedValue.TYPE_FRACTION; 310 } 311 312 public static boolean isDimensionValue(final TypedValue v) { 313 return v.type == TypedValue.TYPE_DIMENSION; 314 } 315 316 public static boolean isIntegerValue(final TypedValue v) { 317 return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT; 318 } 319 320 public static boolean isStringValue(final TypedValue v) { 321 return v.type == TypedValue.TYPE_STRING; 322 } 323 } 324