• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 org.robolectric.shadows;
18 
19 import android.util.TypedValue;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 import org.robolectric.RuntimeEnvironment;
23 import org.robolectric.res.ResName;
24 
25 /**
26  * Helper class to provide various conversion method used in handling android resources.
27  */
28 public final class ResourceHelper {
29 
30   private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)");
31   private final static float[] sFloatOut = new float[1];
32 
33   private final static TypedValue mValue = new TypedValue();
34 
35   /**
36    * Returns the color value represented by the given string value
37    *
38    * @param value the color value
39    * @return the color as an int
40    * @throws NumberFormatException if the conversion failed.
41    */
getColor(String value)42   public static int getColor(String value) {
43     if (value != null) {
44       if (value.startsWith("#") == false) {
45         throw new NumberFormatException(
46             String.format("Color value '%s' must start with #", value));
47       }
48 
49       value = value.substring(1);
50 
51       // make sure it's not longer than 32bit
52       if (value.length() > 8) {
53         throw new NumberFormatException(String.format(
54             "Color value '%s' is too long. Format is either" +
55             "#AARRGGBB, #RRGGBB, #RGB, or #ARGB",
56             value));
57       }
58 
59       if (value.length() == 3) { // RGB format
60         char[] color = new char[8];
61         color[0] = color[1] = 'F';
62         color[2] = color[3] = value.charAt(0);
63         color[4] = color[5] = value.charAt(1);
64         color[6] = color[7] = value.charAt(2);
65         value = new String(color);
66       } else if (value.length() == 4) { // ARGB format
67         char[] color = new char[8];
68         color[0] = color[1] = value.charAt(0);
69         color[2] = color[3] = value.charAt(1);
70         color[4] = color[5] = value.charAt(2);
71         color[6] = color[7] = value.charAt(3);
72         value = new String(color);
73       } else if (value.length() == 6) {
74         value = "FF" + value;
75       }
76 
77       // this is a RRGGBB or AARRGGBB value
78 
79       // Integer.parseInt will fail to inferFromValue strings like "ff191919", so we use
80       // a Long, but cast the result back into an int, since we know that we're only
81       // dealing with 32 bit values.
82       return (int)Long.parseLong(value, 16);
83     }
84 
85     throw new NumberFormatException();
86   }
87 
88   /**
89    * Returns the TypedValue color type represented by the given string value
90    *
91    * @param value the color value
92    * @return the color as an int. For backwards compatibility, will return a default of ARGB8 if
93    *   value format is unrecognized.
94    */
getColorType(String value)95   public static int getColorType(String value) {
96     if (value != null && value.startsWith("#")) {
97       switch (value.length()) {
98         case 4:
99           return TypedValue.TYPE_INT_COLOR_RGB4;
100         case 5:
101           return TypedValue.TYPE_INT_COLOR_ARGB4;
102         case 7:
103           return TypedValue.TYPE_INT_COLOR_RGB8;
104         case 9:
105           return TypedValue.TYPE_INT_COLOR_ARGB8;
106       }
107     }
108     return TypedValue.TYPE_INT_COLOR_ARGB8;
109   }
110 
getInternalResourceId(String idName)111   public static int getInternalResourceId(String idName) {
112     return RuntimeEnvironment.getSystemResourceTable().getResourceId(new ResName("android", "id", idName));
113   }
114 
115   // ------- TypedValue stuff
116   // This is taken from //device/libs/utils/ResourceTypes.cpp
117 
118   private static final class UnitEntry {
119     String name;
120     int type;
121     int unit;
122     float scale;
123 
UnitEntry(String name, int type, int unit, float scale)124     UnitEntry(String name, int type, int unit, float scale) {
125       this.name = name;
126       this.type = type;
127       this.unit = unit;
128       this.scale = scale;
129     }
130   }
131 
132   private final static UnitEntry[] sUnitNames = new UnitEntry[] {
133     new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f),
134     new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
135     new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f),
136     new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f),
137     new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f),
138     new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f),
139     new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f),
140     new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100),
141     new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100),
142   };
143 
144   /**
145    * Returns the raw value from the given attribute float-type value string.
146    * This object is only valid until the next call on to {@link ResourceHelper}.
147    *
148    * @param attribute Attribute name.
149    * @param value Attribute value.
150    * @param requireUnit whether the value is expected to contain a unit.
151    * @return The typed value.
152    */
getValue(String attribute, String value, boolean requireUnit)153   public static TypedValue getValue(String attribute, String value, boolean requireUnit) {
154     if (parseFloatAttribute(attribute, value, mValue, requireUnit)) {
155       return mValue;
156     }
157 
158     return null;
159   }
160 
161   /**
162    * Parse a float attribute and return the parsed value into a given TypedValue.
163    * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false.
164    * @param value the string value of the attribute
165    * @param outValue the TypedValue to receive the parsed value
166    * @param requireUnit whether the value is expected to contain a unit.
167    * @return true if success.
168    */
parseFloatAttribute(String attribute, String value, TypedValue outValue, boolean requireUnit)169   public static boolean parseFloatAttribute(String attribute, String value,
170       TypedValue outValue, boolean requireUnit) {
171     assert requireUnit == false || attribute != null;
172 
173     // remove the space before and after
174     value = value.trim();
175     int len = value.length();
176 
177     if (len <= 0) {
178       return false;
179     }
180 
181     // check that there's no non ascii characters.
182     char[] buf = value.toCharArray();
183     for (int i = 0 ; i < len ; i++) {
184       if (buf[i] > 255) {
185         return false;
186       }
187     }
188 
189     // check the first character
190     if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') {
191       return false;
192     }
193 
194     // now look for the string that is after the float...
195     Matcher m = sFloatPattern.matcher(value);
196     if (m.matches()) {
197       String f_str = m.group(1);
198       String end = m.group(2);
199 
200       float f;
201       try {
202         f = Float.parseFloat(f_str);
203       } catch (NumberFormatException e) {
204         // this shouldn't happen with the regexp above.
205         return false;
206       }
207 
208       if (end.length() > 0 && end.charAt(0) != ' ') {
209         // Might be a unit...
210         if (parseUnit(end, outValue, sFloatOut)) {
211           computeTypedValue(outValue, f, sFloatOut[0]);
212           return true;
213         }
214         return false;
215       }
216 
217       // make sure it's only spaces at the end.
218       end = end.trim();
219 
220       if (end.length() == 0) {
221         if (outValue != null) {
222           outValue.assetCookie = 0;
223           outValue.string = null;
224 
225           if (requireUnit == false) {
226             outValue.type = TypedValue.TYPE_FLOAT;
227             outValue.data = Float.floatToIntBits(f);
228           } else {
229             // no unit when required? Use dp and out an error.
230             applyUnit(sUnitNames[1], outValue, sFloatOut);
231             computeTypedValue(outValue, f, sFloatOut[0]);
232 
233             System.out.println(String.format(
234                 "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!",
235                     value, attribute));
236           }
237           return true;
238         }
239       }
240     }
241 
242     return false;
243   }
244 
computeTypedValue(TypedValue outValue, float value, float scale)245   private static void computeTypedValue(TypedValue outValue, float value, float scale) {
246     value *= scale;
247     boolean neg = value < 0;
248     if (neg) {
249       value = -value;
250     }
251     long bits = (long)(value*(1<<23)+.5f);
252     int radix;
253     int shift;
254     if ((bits&0x7fffff) == 0) {
255       // Always use 23p0 if there is no fraction, just to make
256       // things easier to read.
257       radix = TypedValue.COMPLEX_RADIX_23p0;
258       shift = 23;
259     } else if ((bits&0xffffffffff800000L) == 0) {
260       // Magnitude is zero -- can fit in 0 bits of precision.
261       radix = TypedValue.COMPLEX_RADIX_0p23;
262       shift = 0;
263     } else if ((bits&0xffffffff80000000L) == 0) {
264       // Magnitude can fit in 8 bits of precision.
265       radix = TypedValue.COMPLEX_RADIX_8p15;
266       shift = 8;
267     } else if ((bits&0xffffff8000000000L) == 0) {
268       // Magnitude can fit in 16 bits of precision.
269       radix = TypedValue.COMPLEX_RADIX_16p7;
270       shift = 16;
271     } else {
272       // Magnitude needs entire range, so no fractional part.
273       radix = TypedValue.COMPLEX_RADIX_23p0;
274       shift = 23;
275     }
276     int mantissa = (int)(
277       (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK);
278     if (neg) {
279       mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK;
280     }
281     outValue.data |=
282       (radix<<TypedValue.COMPLEX_RADIX_SHIFT)
283       | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT);
284   }
285 
286   private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) {
287     str = str.trim();
288 
289     for (UnitEntry unit : sUnitNames) {
290       if (unit.name.equals(str)) {
291         applyUnit(unit, outValue, outScale);
292         return true;
293       }
294     }
295 
296     return false;
297   }
298 
299   private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) {
300     outValue.type = unit.type;
301     outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT;
302     outScale[0] = unit.scale;
303   }
304 }
305 
306