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