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