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