• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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