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