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