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.android; 18 19import com.android.ide.common.rendering.api.IProjectCallback; 20import com.android.ide.common.rendering.api.LayoutLog; 21import com.android.ide.common.rendering.api.ResourceValue; 22import com.android.layoutlib.bridge.Bridge; 23import com.android.layoutlib.bridge.BridgeConstants; 24import com.android.layoutlib.bridge.impl.ResourceHelper; 25import com.android.ninepatch.NinePatch; 26import com.android.resources.ResourceType; 27import com.android.util.Pair; 28 29import org.kxml2.io.KXmlParser; 30import org.xmlpull.v1.XmlPullParser; 31import org.xmlpull.v1.XmlPullParserException; 32 33import android.content.res.AssetFileDescriptor; 34import android.content.res.AssetManager; 35import android.content.res.ColorStateList; 36import android.content.res.Configuration; 37import android.content.res.Resources; 38import android.content.res.TypedArray; 39import android.content.res.XmlResourceParser; 40import android.graphics.drawable.Drawable; 41import android.util.AttributeSet; 42import android.util.DisplayMetrics; 43import android.util.TypedValue; 44import android.view.ViewGroup.LayoutParams; 45 46import java.io.File; 47import java.io.FileInputStream; 48import java.io.FileNotFoundException; 49import java.io.InputStream; 50 51/** 52 * 53 */ 54public final class BridgeResources extends Resources { 55 56 private BridgeContext mContext; 57 private IProjectCallback mProjectCallback; 58 private boolean[] mPlatformResourceFlag = new boolean[1]; 59 60 /** 61 * Simpler wrapper around FileInputStream. This is used when the input stream represent 62 * not a normal bitmap but a nine patch. 63 * This is useful when the InputStream is created in a method but used in another that needs 64 * to know whether this is 9-patch or not, such as BitmapFactory. 65 */ 66 public class NinePatchInputStream extends FileInputStream { 67 private boolean mFakeMarkSupport = true; 68 public NinePatchInputStream(File file) throws FileNotFoundException { 69 super(file); 70 } 71 72 @Override 73 public boolean markSupported() { 74 if (mFakeMarkSupport) { 75 // this is needed so that BitmapFactory doesn't wrap this in a BufferedInputStream. 76 return true; 77 } 78 79 return super.markSupported(); 80 } 81 82 public void disableFakeMarkSupport() { 83 // disable fake mark support so that in case codec actually try to use them 84 // we don't lie to them. 85 mFakeMarkSupport = false; 86 } 87 } 88 89 /** 90 * This initializes the static field {@link Resources#mSystem} which is used 91 * by methods who get global resources using {@link Resources#getSystem()}. 92 * <p/> 93 * They will end up using our bridge resources. 94 * <p/> 95 * {@link Bridge} calls this method after setting up a new bridge. 96 */ 97 /*package*/ static Resources initSystem(BridgeContext context, 98 AssetManager assets, 99 DisplayMetrics metrics, 100 Configuration config, 101 IProjectCallback projectCallback) { 102 return Resources.mSystem = new BridgeResources(context, 103 assets, 104 metrics, 105 config, 106 projectCallback); 107 } 108 109 /** 110 * Disposes the static {@link Resources#mSystem} to make sure we don't leave objects 111 * around that would prevent us from unloading the library. 112 */ 113 /*package*/ static void disposeSystem() { 114 if (Resources.mSystem instanceof BridgeResources) { 115 ((BridgeResources)(Resources.mSystem)).mContext = null; 116 ((BridgeResources)(Resources.mSystem)).mProjectCallback = null; 117 } 118 Resources.mSystem = null; 119 } 120 121 private BridgeResources(BridgeContext context, AssetManager assets, DisplayMetrics metrics, 122 Configuration config, IProjectCallback projectCallback) { 123 super(assets, metrics, config); 124 mContext = context; 125 mProjectCallback = projectCallback; 126 } 127 128 public BridgeTypedArray newTypeArray(int numEntries, boolean platformFile) { 129 return new BridgeTypedArray(this, mContext, numEntries, platformFile); 130 } 131 132 private ResourceValue getResourceValue(int id, boolean[] platformResFlag_out) { 133 // first get the String related to this id in the framework 134 Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id); 135 136 if (resourceInfo != null) { 137 platformResFlag_out[0] = true; 138 return mContext.getRenderResources().getFrameworkResource( 139 resourceInfo.getFirst(), resourceInfo.getSecond()); 140 } 141 142 // didn't find a match in the framework? look in the project. 143 if (mProjectCallback != null) { 144 resourceInfo = mProjectCallback.resolveResourceId(id); 145 146 if (resourceInfo != null) { 147 platformResFlag_out[0] = false; 148 return mContext.getRenderResources().getProjectResource( 149 resourceInfo.getFirst(), resourceInfo.getSecond()); 150 } 151 } 152 153 return null; 154 } 155 156 @Override 157 public Drawable getDrawable(int id) throws NotFoundException { 158 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 159 160 if (value != null) { 161 return ResourceHelper.getDrawable(value, mContext); 162 } 163 164 // id was not found or not resolved. Throw a NotFoundException. 165 throwException(id); 166 167 // this is not used since the method above always throws 168 return null; 169 } 170 171 @Override 172 public int getColor(int id) throws NotFoundException { 173 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 174 175 if (value != null) { 176 try { 177 return ResourceHelper.getColor(value.getValue()); 178 } catch (NumberFormatException e) { 179 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, e.getMessage(), e, 180 null /*data*/); 181 return 0; 182 } 183 } 184 185 // id was not found or not resolved. Throw a NotFoundException. 186 throwException(id); 187 188 // this is not used since the method above always throws 189 return 0; 190 } 191 192 @Override 193 public ColorStateList getColorStateList(int id) throws NotFoundException { 194 ResourceValue resValue = getResourceValue(id, mPlatformResourceFlag); 195 196 if (resValue != null) { 197 ColorStateList stateList = ResourceHelper.getColorStateList(resValue, mContext); 198 if (stateList != null) { 199 return stateList; 200 } 201 } 202 203 // id was not found or not resolved. Throw a NotFoundException. 204 throwException(id); 205 206 // this is not used since the method above always throws 207 return null; 208 } 209 210 @Override 211 public CharSequence getText(int id) throws NotFoundException { 212 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 213 214 if (value != null) { 215 return value.getValue(); 216 } 217 218 // id was not found or not resolved. Throw a NotFoundException. 219 throwException(id); 220 221 // this is not used since the method above always throws 222 return null; 223 } 224 225 @Override 226 public XmlResourceParser getLayout(int id) throws NotFoundException { 227 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 228 229 if (value != null) { 230 XmlPullParser parser = null; 231 232 try { 233 // check if the current parser can provide us with a custom parser. 234 if (mPlatformResourceFlag[0] == false) { 235 parser = mProjectCallback.getParser(value.getName()); 236 } 237 238 // create a new one manually if needed. 239 if (parser == null) { 240 File xml = new File(value.getValue()); 241 if (xml.isFile()) { 242 // we need to create a pull parser around the layout XML file, and then 243 // give that to our XmlBlockParser 244 parser = new KXmlParser(); 245 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 246 parser.setInput(new FileInputStream(xml), "UTF-8"); //$NON-NLS-1$); 247 } 248 } 249 250 if (parser != null) { 251 return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); 252 } 253 } catch (XmlPullParserException e) { 254 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 255 "Failed to configure parser for " + value.getValue(), e, null /*data*/); 256 // we'll return null below. 257 } catch (FileNotFoundException e) { 258 // this shouldn't happen since we check above. 259 } 260 261 } 262 263 // id was not found or not resolved. Throw a NotFoundException. 264 throwException(id); 265 266 // this is not used since the method above always throws 267 return null; 268 } 269 270 @Override 271 public XmlResourceParser getAnimation(int id) throws NotFoundException { 272 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 273 274 if (value != null) { 275 XmlPullParser parser = null; 276 277 try { 278 File xml = new File(value.getValue()); 279 if (xml.isFile()) { 280 // we need to create a pull parser around the layout XML file, and then 281 // give that to our XmlBlockParser 282 parser = new KXmlParser(); 283 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 284 parser.setInput(new FileInputStream(xml), "UTF-8"); //$NON-NLS-1$); 285 286 return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); 287 } 288 } catch (XmlPullParserException e) { 289 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 290 "Failed to configure parser for " + value.getValue(), e, null /*data*/); 291 // we'll return null below. 292 } catch (FileNotFoundException e) { 293 // this shouldn't happen since we check above. 294 } 295 296 } 297 298 // id was not found or not resolved. Throw a NotFoundException. 299 throwException(id); 300 301 // this is not used since the method above always throws 302 return null; 303 } 304 305 @Override 306 public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { 307 return mContext.obtainStyledAttributes(set, attrs); 308 } 309 310 @Override 311 public TypedArray obtainTypedArray(int id) throws NotFoundException { 312 throw new UnsupportedOperationException(); 313 } 314 315 316 @Override 317 public float getDimension(int id) throws NotFoundException { 318 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 319 320 if (value != null) { 321 String v = value.getValue(); 322 323 if (v != null) { 324 if (v.equals(BridgeConstants.MATCH_PARENT) || 325 v.equals(BridgeConstants.FILL_PARENT)) { 326 return LayoutParams.MATCH_PARENT; 327 } else if (v.equals(BridgeConstants.WRAP_CONTENT)) { 328 return LayoutParams.WRAP_CONTENT; 329 } 330 331 if (ResourceHelper.stringToFloat(v, mTmpValue) && 332 mTmpValue.type == TypedValue.TYPE_DIMENSION) { 333 return mTmpValue.getDimension(mMetrics); 334 } 335 } 336 } 337 338 // id was not found or not resolved. Throw a NotFoundException. 339 throwException(id); 340 341 // this is not used since the method above always throws 342 return 0; 343 } 344 345 @Override 346 public int getDimensionPixelOffset(int id) throws NotFoundException { 347 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 348 349 if (value != null) { 350 String v = value.getValue(); 351 352 if (v != null) { 353 if (ResourceHelper.stringToFloat(v, mTmpValue) && 354 mTmpValue.type == TypedValue.TYPE_DIMENSION) { 355 return TypedValue.complexToDimensionPixelOffset(mTmpValue.data, mMetrics); 356 } 357 } 358 } 359 360 // id was not found or not resolved. Throw a NotFoundException. 361 throwException(id); 362 363 // this is not used since the method above always throws 364 return 0; 365 } 366 367 @Override 368 public int getDimensionPixelSize(int id) throws NotFoundException { 369 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 370 371 if (value != null) { 372 String v = value.getValue(); 373 374 if (v != null) { 375 if (ResourceHelper.stringToFloat(v, mTmpValue) && 376 mTmpValue.type == TypedValue.TYPE_DIMENSION) { 377 return TypedValue.complexToDimensionPixelSize(mTmpValue.data, mMetrics); 378 } 379 } 380 } 381 382 // id was not found or not resolved. Throw a NotFoundException. 383 throwException(id); 384 385 // this is not used since the method above always throws 386 return 0; 387 } 388 389 @Override 390 public int getInteger(int id) throws NotFoundException { 391 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 392 393 if (value != null && value.getValue() != null) { 394 String v = value.getValue(); 395 int radix = 10; 396 if (v.startsWith("0x")) { 397 v = v.substring(2); 398 radix = 16; 399 } 400 try { 401 return Integer.parseInt(v, radix); 402 } catch (NumberFormatException e) { 403 // return exception below 404 } 405 } 406 407 // id was not found or not resolved. Throw a NotFoundException. 408 throwException(id); 409 410 // this is not used since the method above always throws 411 return 0; 412 } 413 414 @Override 415 public String getResourceEntryName(int resid) throws NotFoundException { 416 throw new UnsupportedOperationException(); 417 } 418 419 @Override 420 public String getResourceName(int resid) throws NotFoundException { 421 throw new UnsupportedOperationException(); 422 } 423 424 @Override 425 public String getResourceTypeName(int resid) throws NotFoundException { 426 throw new UnsupportedOperationException(); 427 } 428 429 @Override 430 public String getString(int id, Object... formatArgs) throws NotFoundException { 431 String s = getString(id); 432 if (s != null) { 433 return String.format(s, formatArgs); 434 435 } 436 437 // id was not found or not resolved. Throw a NotFoundException. 438 throwException(id); 439 440 // this is not used since the method above always throws 441 return null; 442 } 443 444 @Override 445 public String getString(int id) throws NotFoundException { 446 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 447 448 if (value != null && value.getValue() != null) { 449 return value.getValue(); 450 } 451 452 // id was not found or not resolved. Throw a NotFoundException. 453 throwException(id); 454 455 // this is not used since the method above always throws 456 return null; 457 } 458 459 @Override 460 public void getValue(int id, TypedValue outValue, boolean resolveRefs) 461 throws NotFoundException { 462 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 463 464 if (value != null) { 465 String v = value.getValue(); 466 467 if (v != null) { 468 if (ResourceHelper.stringToFloat(v, outValue)) { 469 return; 470 } 471 472 // else it's a string 473 outValue.type = TypedValue.TYPE_STRING; 474 outValue.string = v; 475 return; 476 } 477 } 478 479 // id was not found or not resolved. Throw a NotFoundException. 480 throwException(id); 481 } 482 483 @Override 484 public void getValue(String name, TypedValue outValue, boolean resolveRefs) 485 throws NotFoundException { 486 throw new UnsupportedOperationException(); 487 } 488 489 @Override 490 public XmlResourceParser getXml(int id) throws NotFoundException { 491 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 492 493 if (value != null) { 494 String v = value.getValue(); 495 496 if (v != null) { 497 // check this is a file 498 File f = new File(value.getValue()); 499 if (f.isFile()) { 500 try { 501 KXmlParser parser = new KXmlParser(); 502 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 503 parser.setInput(new FileInputStream(f), "UTF-8"); //$NON-NLS-1$); 504 505 return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); 506 } catch (XmlPullParserException e) { 507 NotFoundException newE = new NotFoundException(); 508 newE.initCause(e); 509 throw newE; 510 } catch (FileNotFoundException e) { 511 NotFoundException newE = new NotFoundException(); 512 newE.initCause(e); 513 throw newE; 514 } 515 } 516 } 517 } 518 519 // id was not found or not resolved. Throw a NotFoundException. 520 throwException(id); 521 522 // this is not used since the method above always throws 523 return null; 524 } 525 526 @Override 527 public XmlResourceParser loadXmlResourceParser(String file, int id, 528 int assetCookie, String type) throws NotFoundException { 529 // even though we know the XML file to load directly, we still need to resolve the 530 // id so that we can know if it's a platform or project resource. 531 // (mPlatformResouceFlag will get the result and will be used later). 532 getResourceValue(id, mPlatformResourceFlag); 533 534 File f = new File(file); 535 try { 536 KXmlParser parser = new KXmlParser(); 537 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 538 parser.setInput(new FileInputStream(f), "UTF-8"); //$NON-NLS-1$); 539 540 return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); 541 } catch (XmlPullParserException e) { 542 NotFoundException newE = new NotFoundException(); 543 newE.initCause(e); 544 throw newE; 545 } catch (FileNotFoundException e) { 546 NotFoundException newE = new NotFoundException(); 547 newE.initCause(e); 548 throw newE; 549 } 550 } 551 552 553 @Override 554 public InputStream openRawResource(int id) throws NotFoundException { 555 ResourceValue value = getResourceValue(id, mPlatformResourceFlag); 556 557 if (value != null) { 558 String path = value.getValue(); 559 560 if (path != null) { 561 // check this is a file 562 File f = new File(path); 563 if (f.isFile()) { 564 try { 565 // if it's a nine-patch return a custom input stream so that 566 // other methods (mainly bitmap factory) can detect it's a 9-patch 567 // and actually load it as a 9-patch instead of a normal bitmap 568 if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) { 569 return new NinePatchInputStream(f); 570 } 571 return new FileInputStream(f); 572 } catch (FileNotFoundException e) { 573 NotFoundException newE = new NotFoundException(); 574 newE.initCause(e); 575 throw newE; 576 } 577 } 578 } 579 } 580 581 // id was not found or not resolved. Throw a NotFoundException. 582 throwException(id); 583 584 // this is not used since the method above always throws 585 return null; 586 } 587 588 @Override 589 public InputStream openRawResource(int id, TypedValue value) throws NotFoundException { 590 getValue(id, value, true); 591 592 String path = value.string.toString(); 593 594 File f = new File(path); 595 if (f.isFile()) { 596 try { 597 // if it's a nine-patch return a custom input stream so that 598 // other methods (mainly bitmap factory) can detect it's a 9-patch 599 // and actually load it as a 9-patch instead of a normal bitmap 600 if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) { 601 return new NinePatchInputStream(f); 602 } 603 return new FileInputStream(f); 604 } catch (FileNotFoundException e) { 605 NotFoundException exception = new NotFoundException(); 606 exception.initCause(e); 607 throw exception; 608 } 609 } 610 611 throw new NotFoundException(); 612 } 613 614 @Override 615 public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException { 616 throw new UnsupportedOperationException(); 617 } 618 619 /** 620 * Builds and throws a {@link Resources.NotFoundException} based on a resource id and a resource type. 621 * @param id the id of the resource 622 * @throws NotFoundException 623 */ 624 private void throwException(int id) throws NotFoundException { 625 // first get the String related to this id in the framework 626 Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id); 627 628 // if the name is unknown in the framework, get it from the custom view loader. 629 if (resourceInfo == null && mProjectCallback != null) { 630 resourceInfo = mProjectCallback.resolveResourceId(id); 631 } 632 633 String message = null; 634 if (resourceInfo != null) { 635 message = String.format( 636 "Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.", 637 resourceInfo.getFirst(), id, resourceInfo.getSecond()); 638 } else { 639 message = String.format( 640 "Could not resolve resource value: 0x%1$X.", id); 641 } 642 643 throw new NotFoundException(message); 644 } 645} 646