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