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 import com.android.inputmethod.latin.settings.SettingsValues;
30 
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.regex.PatternSyntaxException;
34 
35 public 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 
ResourceUtils()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];
sBuildKeyValues.put(key, value)64             sBuildKeyValues.put(key, value);
65             keyValuePairs.add(key + '=' + value);
66         }
67         sBuildKeyValuesDebugString = "[" + TextUtils.join(" ", keyValuePairs) + "]";
68     }
69 
getDeviceOverrideValue(final Resources res, final int overrideResId, final String defaultValue)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 {
DeviceOverridePatternSyntaxError(final String message, final String expression)96         public DeviceOverridePatternSyntaxError(final String message, final String expression) {
97             this(message, expression, null);
98         }
99 
DeviceOverridePatternSyntaxError(final String message, final String expression, final Throwable throwable)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
findConstantForKeyValuePairs(final HashMap<String, String> keyValuePairs, final String[] conditionConstantArray)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 
fulfillsCondition(final HashMap<String,String> keyValuePairs, final String condition)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 
getDefaultKeyboardWidth(final Resources res)185     public static int getDefaultKeyboardWidth(final Resources res) {
186         final DisplayMetrics dm = res.getDisplayMetrics();
187         return dm.widthPixels;
188     }
189 
getKeyboardHeight(final Resources res, final SettingsValues settingsValues)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 
getDefaultKeyboardHeight(final Resources res)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 
isValidFraction(final float fraction)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.
isValidDimensionPixelSize(final int dimension)229     public static boolean isValidDimensionPixelSize(final int dimension) {
230         return dimension > 0;
231     }
232 
233     // {@link Resources#getDimensionPixelOffset(int)} may return zero pixel offset.
isValidDimensionPixelOffset(final int dimension)234     public static boolean isValidDimensionPixelOffset(final int dimension) {
235         return dimension >= 0;
236     }
237 
getFloatFromFraction(final Resources res, final int fractionResId)238     public static float getFloatFromFraction(final Resources res, final int fractionResId) {
239         return res.getFraction(fractionResId, 1, 1);
240     }
241 
getFraction(final TypedArray a, final int index, final float defValue)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 
getFraction(final TypedArray a, final int index)250     public static float getFraction(final TypedArray a, final int index) {
251         return getFraction(a, index, UNDEFINED_RATIO);
252     }
253 
getDimensionPixelSize(final TypedArray a, final int index)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 
getDimensionOrFraction(final TypedArray a, final int index, final int base, final float defValue)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 
getEnumValue(final TypedArray a, final int index, final int defValue)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 
isFractionValue(final TypedValue v)287     public static boolean isFractionValue(final TypedValue v) {
288         return v.type == TypedValue.TYPE_FRACTION;
289     }
290 
isDimensionValue(final TypedValue v)291     public static boolean isDimensionValue(final TypedValue v) {
292         return v.type == TypedValue.TYPE_DIMENSION;
293     }
294 
isIntegerValue(final TypedValue v)295     public static boolean isIntegerValue(final TypedValue v) {
296         return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
297     }
298 
isStringValue(final TypedValue v)299     public static boolean isStringValue(final TypedValue v) {
300         return v.type == TypedValue.TYPE_STRING;
301     }
302 }
303