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