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