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