Home | History | Annotate | Download | only in utils
      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 
     40     private ResourceUtils() {
     41         // This utility class is not publicly instantiable.
     42     }
     43 
     44     private static final HashMap<String, String> sDeviceOverrideValueMap =
     45             CollectionUtils.newHashMap();
     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 = CollectionUtils.newHashMap();
     58         final ArrayList<String> keyValuePairs = CollectionUtils.newArrayList();
     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 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         String defaultValue = null;
     90         try {
     91             defaultValue = findDefaultConstant(overrideArray);
     92             // The defaultValue might be an empty string.
     93             if (defaultValue == null) {
     94                 Log.w(TAG, "Couldn't find override value nor default value:"
     95                         + " resource="+ res.getResourceEntryName(overrideResId)
     96                         + " build=" + sBuildKeyValuesDebugString);
     97             } else {
     98                 Log.i(TAG, "Found default value:"
     99                         + " resource="+ res.getResourceEntryName(overrideResId)
    100                         + " build=" + sBuildKeyValuesDebugString
    101                         + " default=" + defaultValue);
    102             }
    103         } catch (final DeviceOverridePatternSyntaxError e) {
    104             Log.w(TAG, "Syntax error, ignored", e);
    105         }
    106         sDeviceOverrideValueMap.put(key, defaultValue);
    107         return defaultValue;
    108     }
    109 
    110     @SuppressWarnings("serial")
    111     static class DeviceOverridePatternSyntaxError extends Exception {
    112         public DeviceOverridePatternSyntaxError(final String message, final String expression) {
    113             this(message, expression, null);
    114         }
    115 
    116         public DeviceOverridePatternSyntaxError(final String message, final String expression,
    117                 final Throwable throwable) {
    118             super(message + ": " + expression, throwable);
    119         }
    120     }
    121 
    122     /**
    123      * Find the condition that fulfills specified key value pairs from an array of
    124      * "condition,constant", and return the corresponding string constant. A condition is
    125      * "pattern1[:pattern2...] (or an empty string for the default). A pattern is
    126      * "key=regexp_value" string. The condition matches only if all patterns of the condition
    127      * are true for the specified key value pairs.
    128      *
    129      * For example, "condition,constant" has the following format.
    130      * (See {@link ResourceUtilsTests#testFindConstantForKeyValuePairsRegexp()})
    131      *  - HARDWARE=mako,constantForNexus4
    132      *  - MODEL=Nexus 4:MANUFACTURER=LGE,constantForNexus4
    133      *  - ,defaultConstant
    134      *
    135      * @param keyValuePairs attributes to be used to look for a matched condition.
    136      * @param conditionConstantArray an array of "condition,constant" elements to be searched.
    137      * @return the constant part of the matched "condition,constant" element. Returns null if no
    138      * condition matches.
    139      */
    140     @UsedForTesting
    141     static String findConstantForKeyValuePairs(final HashMap<String, String> keyValuePairs,
    142             final String[] conditionConstantArray) {
    143         if (conditionConstantArray == null || keyValuePairs == null) {
    144             return null;
    145         }
    146         String foundValue = null;
    147         for (final String conditionConstant : conditionConstantArray) {
    148             final int posComma = conditionConstant.indexOf(',');
    149             if (posComma < 0) {
    150                 Log.w(TAG, "Array element has no comma: " + conditionConstant);
    151                 continue;
    152             }
    153             final String condition = conditionConstant.substring(0, posComma);
    154             if (condition.isEmpty()) {
    155                 // Default condition. The default condition should be searched by
    156                 // {@link #findConstantForDefault(String[])}.
    157                 continue;
    158             }
    159             try {
    160                 if (fulfillsCondition(keyValuePairs, condition)) {
    161                     // Take first match
    162                     if (foundValue == null) {
    163                         foundValue = conditionConstant.substring(posComma + 1);
    164                     }
    165                     // And continue walking through all conditions.
    166                 }
    167             } catch (final DeviceOverridePatternSyntaxError e) {
    168                 Log.w(TAG, "Syntax error, ignored", e);
    169             }
    170         }
    171         return foundValue;
    172     }
    173 
    174     private static boolean fulfillsCondition(final HashMap<String,String> keyValuePairs,
    175             final String condition) throws DeviceOverridePatternSyntaxError {
    176         final String[] patterns = condition.split(":");
    177         // Check all patterns in a condition are true
    178         boolean matchedAll = true;
    179         for (final String pattern : patterns) {
    180             final int posEqual = pattern.indexOf('=');
    181             if (posEqual < 0) {
    182                 throw new DeviceOverridePatternSyntaxError("Pattern has no '='", condition);
    183             }
    184             final String key = pattern.substring(0, posEqual);
    185             final String value = keyValuePairs.get(key);
    186             if (value == null) {
    187                 throw new DeviceOverridePatternSyntaxError("Unknown key", condition);
    188             }
    189             final String patternRegexpValue = pattern.substring(posEqual + 1);
    190             try {
    191                 if (!value.matches(patternRegexpValue)) {
    192                     matchedAll = false;
    193                     // And continue walking through all patterns.
    194                 }
    195             } catch (final PatternSyntaxException e) {
    196                 throw new DeviceOverridePatternSyntaxError("Syntax error", condition, e);
    197             }
    198         }
    199         return matchedAll;
    200     }
    201 
    202     @UsedForTesting
    203     static String findDefaultConstant(final String[] conditionConstantArray)
    204             throws DeviceOverridePatternSyntaxError {
    205         if (conditionConstantArray == null) {
    206             return null;
    207         }
    208         for (final String condition : conditionConstantArray) {
    209             final int posComma = condition.indexOf(',');
    210             if (posComma < 0) {
    211                 throw new DeviceOverridePatternSyntaxError("Array element has no comma", condition);
    212             }
    213             if (posComma == 0) { // condition is empty.
    214                 return condition.substring(posComma + 1);
    215             }
    216         }
    217         return null;
    218     }
    219 
    220     public static int getDefaultKeyboardWidth(final Resources res) {
    221         final DisplayMetrics dm = res.getDisplayMetrics();
    222         return dm.widthPixels;
    223     }
    224 
    225     public static int getDefaultKeyboardHeight(final Resources res) {
    226         final DisplayMetrics dm = res.getDisplayMetrics();
    227         final String keyboardHeightString = getDeviceOverrideValue(res, R.array.keyboard_heights);
    228         final float keyboardHeight;
    229         if (TextUtils.isEmpty(keyboardHeightString)) {
    230             keyboardHeight = res.getDimension(R.dimen.keyboardHeight);
    231         } else {
    232             keyboardHeight = Float.parseFloat(keyboardHeightString) * dm.density;
    233         }
    234         final float maxKeyboardHeight = res.getFraction(
    235                 R.fraction.maxKeyboardHeight, dm.heightPixels, dm.heightPixels);
    236         float minKeyboardHeight = res.getFraction(
    237                 R.fraction.minKeyboardHeight, dm.heightPixels, dm.heightPixels);
    238         if (minKeyboardHeight < 0.0f) {
    239             // Specified fraction was negative, so it should be calculated against display
    240             // width.
    241             minKeyboardHeight = -res.getFraction(
    242                     R.fraction.minKeyboardHeight, dm.widthPixels, dm.widthPixels);
    243         }
    244         // Keyboard height will not exceed maxKeyboardHeight and will not be less than
    245         // minKeyboardHeight.
    246         return (int)Math.max(Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
    247     }
    248 
    249     public static boolean isValidFraction(final float fraction) {
    250         return fraction >= 0.0f;
    251     }
    252 
    253     // {@link Resources#getDimensionPixelSize(int)} returns at least one pixel size.
    254     public static boolean isValidDimensionPixelSize(final int dimension) {
    255         return dimension > 0;
    256     }
    257 
    258     // {@link Resources#getDimensionPixelOffset(int)} may return zero pixel offset.
    259     public static boolean isValidDimensionPixelOffset(final int dimension) {
    260         return dimension >= 0;
    261     }
    262 
    263     public static float getFraction(final TypedArray a, final int index, final float defValue) {
    264         final TypedValue value = a.peekValue(index);
    265         if (value == null || !isFractionValue(value)) {
    266             return defValue;
    267         }
    268         return a.getFraction(index, 1, 1, defValue);
    269     }
    270 
    271     public static float getFraction(final TypedArray a, final int index) {
    272         return getFraction(a, index, UNDEFINED_RATIO);
    273     }
    274 
    275     public static int getDimensionPixelSize(final TypedArray a, final int index) {
    276         final TypedValue value = a.peekValue(index);
    277         if (value == null || !isDimensionValue(value)) {
    278             return ResourceUtils.UNDEFINED_DIMENSION;
    279         }
    280         return a.getDimensionPixelSize(index, ResourceUtils.UNDEFINED_DIMENSION);
    281     }
    282 
    283     public static float getDimensionOrFraction(final TypedArray a, final int index, final int base,
    284             final float defValue) {
    285         final TypedValue value = a.peekValue(index);
    286         if (value == null) {
    287             return defValue;
    288         }
    289         if (isFractionValue(value)) {
    290             return a.getFraction(index, base, base, defValue);
    291         } else if (isDimensionValue(value)) {
    292             return a.getDimension(index, defValue);
    293         }
    294         return defValue;
    295     }
    296 
    297     public static int getEnumValue(final TypedArray a, final int index, final int defValue) {
    298         final TypedValue value = a.peekValue(index);
    299         if (value == null) {
    300             return defValue;
    301         }
    302         if (isIntegerValue(value)) {
    303             return a.getInt(index, defValue);
    304         }
    305         return defValue;
    306     }
    307 
    308     public static boolean isFractionValue(final TypedValue v) {
    309         return v.type == TypedValue.TYPE_FRACTION;
    310     }
    311 
    312     public static boolean isDimensionValue(final TypedValue v) {
    313         return v.type == TypedValue.TYPE_DIMENSION;
    314     }
    315 
    316     public static boolean isIntegerValue(final TypedValue v) {
    317         return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
    318     }
    319 
    320     public static boolean isStringValue(final TypedValue v) {
    321         return v.type == TypedValue.TYPE_STRING;
    322     }
    323 }
    324