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