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