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 com.android.layoutlib.bridge.impl; 18 19 import com.android.ide.common.rendering.api.DensityBasedResourceValue; 20 import com.android.ide.common.rendering.api.LayoutLog; 21 import com.android.ide.common.rendering.api.RenderResources; 22 import com.android.ide.common.rendering.api.ResourceValue; 23 import com.android.layoutlib.bridge.Bridge; 24 import com.android.layoutlib.bridge.android.BridgeContext; 25 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 26 import com.android.ninepatch.NinePatch; 27 import com.android.ninepatch.NinePatchChunk; 28 import com.android.resources.Density; 29 30 import org.xmlpull.v1.XmlPullParser; 31 import org.xmlpull.v1.XmlPullParserException; 32 33 import android.content.res.ColorStateList; 34 import android.graphics.Bitmap; 35 import android.graphics.Bitmap_Delegate; 36 import android.graphics.NinePatch_Delegate; 37 import android.graphics.Rect; 38 import android.graphics.drawable.BitmapDrawable; 39 import android.graphics.drawable.ColorDrawable; 40 import android.graphics.drawable.Drawable; 41 import android.graphics.drawable.NinePatchDrawable; 42 import android.util.TypedValue; 43 44 import java.io.File; 45 import java.io.FileInputStream; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.net.MalformedURLException; 49 import java.util.regex.Matcher; 50 import java.util.regex.Pattern; 51 52 /** 53 * Helper class to provide various conversion method used in handling android resources. 54 */ 55 public final class ResourceHelper { 56 57 private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); 58 private final static float[] sFloatOut = new float[1]; 59 60 private final static TypedValue mValue = new TypedValue(); 61 62 /** 63 * Returns the color value represented by the given string value 64 * @param value the color value 65 * @return the color as an int 66 * @throw NumberFormatException if the conversion failed. 67 */ 68 public static int getColor(String value) { 69 if (value != null) { 70 if (value.startsWith("#") == false) { 71 throw new NumberFormatException( 72 String.format("Color value '%s' must start with #", value)); 73 } 74 75 value = value.substring(1); 76 77 // make sure it's not longer than 32bit 78 if (value.length() > 8) { 79 throw new NumberFormatException(String.format( 80 "Color value '%s' is too long. Format is either" + 81 "#AARRGGBB, #RRGGBB, #RGB, or #ARGB", 82 value)); 83 } 84 85 if (value.length() == 3) { // RGB format 86 char[] color = new char[8]; 87 color[0] = color[1] = 'F'; 88 color[2] = color[3] = value.charAt(0); 89 color[4] = color[5] = value.charAt(1); 90 color[6] = color[7] = value.charAt(2); 91 value = new String(color); 92 } else if (value.length() == 4) { // ARGB format 93 char[] color = new char[8]; 94 color[0] = color[1] = value.charAt(0); 95 color[2] = color[3] = value.charAt(1); 96 color[4] = color[5] = value.charAt(2); 97 color[6] = color[7] = value.charAt(3); 98 value = new String(color); 99 } else if (value.length() == 6) { 100 value = "FF" + value; 101 } 102 103 // this is a RRGGBB or AARRGGBB value 104 105 // Integer.parseInt will fail to parse strings like "ff191919", so we use 106 // a Long, but cast the result back into an int, since we know that we're only 107 // dealing with 32 bit values. 108 return (int)Long.parseLong(value, 16); 109 } 110 111 throw new NumberFormatException(); 112 } 113 114 public static ColorStateList getColorStateList(ResourceValue resValue, BridgeContext context) { 115 String value = resValue.getValue(); 116 if (value != null && RenderResources.REFERENCE_NULL.equals(value) == false) { 117 // first check if the value is a file (xml most likely) 118 File f = new File(value); 119 if (f.isFile()) { 120 try { 121 // let the framework inflate the ColorStateList from the XML file, by 122 // providing an XmlPullParser 123 XmlPullParser parser = ParserFactory.create(f); 124 125 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 126 parser, context, resValue.isFramework()); 127 try { 128 return ColorStateList.createFromXml(context.getResources(), blockParser); 129 } finally { 130 blockParser.ensurePopped(); 131 } 132 } catch (XmlPullParserException e) { 133 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 134 "Failed to configure parser for " + value, e, null /*data*/); 135 // we'll return null below. 136 } catch (Exception e) { 137 // this is an error and not warning since the file existence is 138 // checked before attempting to parse it. 139 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 140 "Failed to parse file " + value, e, null /*data*/); 141 142 return null; 143 } 144 } else { 145 // try to load the color state list from an int 146 try { 147 int color = ResourceHelper.getColor(value); 148 return ColorStateList.valueOf(color); 149 } catch (NumberFormatException e) { 150 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, 151 "Failed to convert " + value + " into a ColorStateList", e, 152 null /*data*/); 153 return null; 154 } 155 } 156 } 157 158 return null; 159 } 160 161 /** 162 * Returns a drawable from the given value. 163 * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, 164 * or an hexadecimal color 165 * @param context the current context 166 */ 167 public static Drawable getDrawable(ResourceValue value, BridgeContext context) { 168 String stringValue = value.getValue(); 169 if (RenderResources.REFERENCE_NULL.equals(stringValue)) { 170 return null; 171 } 172 173 String lowerCaseValue = stringValue.toLowerCase(); 174 175 Density density = Density.MEDIUM; 176 if (value instanceof DensityBasedResourceValue) { 177 density = 178 ((DensityBasedResourceValue)value).getResourceDensity(); 179 } 180 181 182 if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { 183 File file = new File(stringValue); 184 if (file.isFile()) { 185 try { 186 return getNinePatchDrawable( 187 new FileInputStream(file), density, value.isFramework(), 188 stringValue, context); 189 } catch (IOException e) { 190 // failed to read the file, we'll return null below. 191 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 192 "Failed lot load " + file.getAbsolutePath(), e, null /*data*/); 193 } 194 } 195 196 return null; 197 } else if (lowerCaseValue.endsWith(".xml")) { 198 // create a block parser for the file 199 File f = new File(stringValue); 200 if (f.isFile()) { 201 try { 202 // let the framework inflate the Drawable from the XML file. 203 XmlPullParser parser = ParserFactory.create(f); 204 205 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 206 parser, context, value.isFramework()); 207 try { 208 return Drawable.createFromXml(context.getResources(), blockParser); 209 } finally { 210 blockParser.ensurePopped(); 211 } 212 } catch (Exception e) { 213 // this is an error and not warning since the file existence is checked before 214 // attempting to parse it. 215 Bridge.getLog().error(null, "Failed to parse file " + stringValue, 216 e, null /*data*/); 217 } 218 } else { 219 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 220 String.format("File %s does not exist (or is not a file)", stringValue), 221 null /*data*/); 222 } 223 224 return null; 225 } else { 226 File bmpFile = new File(stringValue); 227 if (bmpFile.isFile()) { 228 try { 229 Bitmap bitmap = Bridge.getCachedBitmap(stringValue, 230 value.isFramework() ? null : context.getProjectKey()); 231 232 if (bitmap == null) { 233 bitmap = Bitmap_Delegate.createBitmap(bmpFile, false /*isMutable*/, 234 density); 235 Bridge.setCachedBitmap(stringValue, bitmap, 236 value.isFramework() ? null : context.getProjectKey()); 237 } 238 239 return new BitmapDrawable(context.getResources(), bitmap); 240 } catch (IOException e) { 241 // we'll return null below 242 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 243 "Failed lot load " + bmpFile.getAbsolutePath(), e, null /*data*/); 244 } 245 } else { 246 // attempt to get a color from the value 247 try { 248 int color = getColor(stringValue); 249 return new ColorDrawable(color); 250 } catch (NumberFormatException e) { 251 // we'll return null below. 252 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, 253 "Failed to convert " + stringValue + " into a drawable", e, 254 null /*data*/); 255 } 256 } 257 } 258 259 return null; 260 } 261 262 private static Drawable getNinePatchDrawable(InputStream inputStream, Density density, 263 boolean isFramework, String cacheKey, BridgeContext context) throws IOException { 264 // see if we still have both the chunk and the bitmap in the caches 265 NinePatchChunk chunk = Bridge.getCached9Patch(cacheKey, 266 isFramework ? null : context.getProjectKey()); 267 Bitmap bitmap = Bridge.getCachedBitmap(cacheKey, 268 isFramework ? null : context.getProjectKey()); 269 270 // if either chunk or bitmap is null, then we reload the 9-patch file. 271 if (chunk == null || bitmap == null) { 272 try { 273 NinePatch ninePatch = NinePatch.load(inputStream, true /*is9Patch*/, 274 false /* convert */); 275 if (ninePatch != null) { 276 if (chunk == null) { 277 chunk = ninePatch.getChunk(); 278 279 Bridge.setCached9Patch(cacheKey, chunk, 280 isFramework ? null : context.getProjectKey()); 281 } 282 283 if (bitmap == null) { 284 bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(), 285 false /*isMutable*/, 286 density); 287 288 Bridge.setCachedBitmap(cacheKey, bitmap, 289 isFramework ? null : context.getProjectKey()); 290 } 291 } 292 } catch (MalformedURLException e) { 293 // URL is wrong, we'll return null below 294 } 295 } 296 297 if (chunk != null && bitmap != null) { 298 int[] padding = chunk.getPadding(); 299 Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]); 300 301 return new NinePatchDrawable(context.getResources(), bitmap, 302 NinePatch_Delegate.serialize(chunk), 303 paddingRect, null); 304 } 305 306 return null; 307 } 308 309 // ------- TypedValue stuff 310 // This is taken from //device/libs/utils/ResourceTypes.cpp 311 312 private static final class UnitEntry { 313 String name; 314 int type; 315 int unit; 316 float scale; 317 318 UnitEntry(String name, int type, int unit, float scale) { 319 this.name = name; 320 this.type = type; 321 this.unit = unit; 322 this.scale = scale; 323 } 324 } 325 326 private final static UnitEntry[] sUnitNames = new UnitEntry[] { 327 new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), 328 new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 329 new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 330 new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), 331 new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), 332 new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), 333 new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), 334 new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), 335 new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), 336 }; 337 338 /** 339 * Returns the raw value from the given attribute float-type value string. 340 * This object is only valid until the next call on to {@link ResourceHelper}. 341 */ 342 public static TypedValue getValue(String attribute, String value, boolean requireUnit) { 343 if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { 344 return mValue; 345 } 346 347 return null; 348 } 349 350 /** 351 * Parse a float attribute and return the parsed value into a given TypedValue. 352 * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false. 353 * @param value the string value of the attribute 354 * @param outValue the TypedValue to receive the parsed value 355 * @param requireUnit whether the value is expected to contain a unit. 356 * @return true if success. 357 */ 358 public static boolean parseFloatAttribute(String attribute, String value, 359 TypedValue outValue, boolean requireUnit) { 360 assert requireUnit == false || attribute != null; 361 362 // remove the space before and after 363 value = value.trim(); 364 int len = value.length(); 365 366 if (len <= 0) { 367 return false; 368 } 369 370 // check that there's no non ascii characters. 371 char[] buf = value.toCharArray(); 372 for (int i = 0 ; i < len ; i++) { 373 if (buf[i] > 255) { 374 return false; 375 } 376 } 377 378 // check the first character 379 if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') { 380 return false; 381 } 382 383 // now look for the string that is after the float... 384 Matcher m = sFloatPattern.matcher(value); 385 if (m.matches()) { 386 String f_str = m.group(1); 387 String end = m.group(2); 388 389 float f; 390 try { 391 f = Float.parseFloat(f_str); 392 } catch (NumberFormatException e) { 393 // this shouldn't happen with the regexp above. 394 return false; 395 } 396 397 if (end.length() > 0 && end.charAt(0) != ' ') { 398 // Might be a unit... 399 if (parseUnit(end, outValue, sFloatOut)) { 400 computeTypedValue(outValue, f, sFloatOut[0]); 401 return true; 402 } 403 return false; 404 } 405 406 // make sure it's only spaces at the end. 407 end = end.trim(); 408 409 if (end.length() == 0) { 410 if (outValue != null) { 411 if (requireUnit == false) { 412 outValue.type = TypedValue.TYPE_FLOAT; 413 outValue.data = Float.floatToIntBits(f); 414 } else { 415 // no unit when required? Use dp and out an error. 416 applyUnit(sUnitNames[1], outValue, sFloatOut); 417 computeTypedValue(outValue, f, sFloatOut[0]); 418 419 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE, 420 String.format( 421 "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", 422 value, attribute), 423 null); 424 } 425 return true; 426 } 427 } 428 } 429 430 return false; 431 } 432 433 private static void computeTypedValue(TypedValue outValue, float value, float scale) { 434 value *= scale; 435 boolean neg = value < 0; 436 if (neg) { 437 value = -value; 438 } 439 long bits = (long)(value*(1<<23)+.5f); 440 int radix; 441 int shift; 442 if ((bits&0x7fffff) == 0) { 443 // Always use 23p0 if there is no fraction, just to make 444 // things easier to read. 445 radix = TypedValue.COMPLEX_RADIX_23p0; 446 shift = 23; 447 } else if ((bits&0xffffffffff800000L) == 0) { 448 // Magnitude is zero -- can fit in 0 bits of precision. 449 radix = TypedValue.COMPLEX_RADIX_0p23; 450 shift = 0; 451 } else if ((bits&0xffffffff80000000L) == 0) { 452 // Magnitude can fit in 8 bits of precision. 453 radix = TypedValue.COMPLEX_RADIX_8p15; 454 shift = 8; 455 } else if ((bits&0xffffff8000000000L) == 0) { 456 // Magnitude can fit in 16 bits of precision. 457 radix = TypedValue.COMPLEX_RADIX_16p7; 458 shift = 16; 459 } else { 460 // Magnitude needs entire range, so no fractional part. 461 radix = TypedValue.COMPLEX_RADIX_23p0; 462 shift = 23; 463 } 464 int mantissa = (int)( 465 (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); 466 if (neg) { 467 mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; 468 } 469 outValue.data |= 470 (radix<<TypedValue.COMPLEX_RADIX_SHIFT) 471 | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); 472 } 473 474 private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { 475 str = str.trim(); 476 477 for (UnitEntry unit : sUnitNames) { 478 if (unit.name.equals(str)) { 479 applyUnit(unit, outValue, outScale); 480 return true; 481 } 482 } 483 484 return false; 485 } 486 487 private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) { 488 outValue.type = unit.type; 489 outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; 490 outScale[0] = unit.scale; 491 } 492 } 493 494