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