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