ShadowAssetManager.java revision 793ee1db287b053127b6e60891c3dbfd1ce4bc54
1package org.robolectric.shadows; 2 3import android.content.res.AssetFileDescriptor; 4import android.content.res.AssetManager; 5import android.content.res.Resources; 6import android.content.res.TypedArray; 7import android.content.res.XmlResourceParser; 8import android.os.ParcelFileDescriptor; 9import android.util.AttributeSet; 10import android.util.SparseArray; 11import android.util.TypedValue; 12import javax.annotation.Nonnull; 13import org.robolectric.RuntimeEnvironment; 14import org.robolectric.android.XmlResourceParserImpl; 15import org.robolectric.annotation.HiddenApi; 16import org.robolectric.annotation.Implementation; 17import org.robolectric.annotation.Implements; 18import org.robolectric.annotation.RealObject; 19import org.robolectric.annotation.Resetter; 20import org.robolectric.res.AttrData; 21import org.robolectric.res.AttributeResource; 22import org.robolectric.res.DrawableResourceLoader; 23import org.robolectric.res.EmptyStyle; 24import org.robolectric.res.FileTypedResource; 25import org.robolectric.res.Fs; 26import org.robolectric.res.FsFile; 27import org.robolectric.res.ResName; 28import org.robolectric.res.ResType; 29import org.robolectric.res.ResourceIds; 30import org.robolectric.res.ResourceTable; 31import org.robolectric.res.Style; 32import org.robolectric.res.StyleData; 33import org.robolectric.res.StyleResolver; 34import org.robolectric.res.ThemeStyleSet; 35import org.robolectric.res.TypedResource; 36import org.robolectric.res.builder.XmlBlock; 37import org.robolectric.util.Logger; 38import org.robolectric.util.ReflectionHelpers; 39 40import java.io.ByteArrayInputStream; 41import java.io.File; 42import java.io.IOException; 43import java.io.InputStream; 44import java.util.HashMap; 45import java.util.List; 46import java.util.Map; 47 48import static android.os.Build.VERSION_CODES.KITKAT_WATCH; 49import static android.os.Build.VERSION_CODES.LOLLIPOP; 50import static org.robolectric.RuntimeEnvironment.castNativePtr; 51import static org.robolectric.Shadows.shadowOf; 52 53@Implements(AssetManager.class) 54public final class ShadowAssetManager { 55 public static final int STYLE_NUM_ENTRIES = 6; 56 public static final int STYLE_TYPE = 0; 57 public static final int STYLE_DATA = 1; 58 public static final int STYLE_ASSET_COOKIE = 2; 59 public static final int STYLE_RESOURCE_ID = 3; 60 public static final int STYLE_CHANGING_CONFIGURATIONS = 4; 61 public static final int STYLE_DENSITY = 5; 62 63 boolean strictErrors = false; 64 65 private static long nextInternalThemeId = 1000; 66 private static final Map<Long, NativeTheme> nativeThemes = new HashMap<>(); 67 private ResourceTable resourceTable; 68 69 class NativeTheme { 70 private ThemeStyleSet themeStyleSet; 71 72 public NativeTheme(ThemeStyleSet themeStyleSet) { 73 this.themeStyleSet = themeStyleSet; 74 } 75 76 public ShadowAssetManager getShadowAssetManager() { 77 return ShadowAssetManager.this; 78 } 79 } 80 81 @RealObject 82 AssetManager realObject; 83 84 private void convertAndFill(AttributeResource attribute, TypedValue outValue, String qualifiers, boolean resolveRefs) { 85 if (attribute.isNull()) { 86 outValue.type = TypedValue.TYPE_NULL; 87 outValue.data = TypedValue.DATA_NULL_UNDEFINED; 88 return; 89 } else if (attribute.isEmpty()) { 90 outValue.type = TypedValue.TYPE_NULL; 91 outValue.data = TypedValue.DATA_NULL_EMPTY; 92 return; 93 } 94 95 // short-circuit Android caching of loaded resources cuz our string positions don't remain stable... 96 outValue.assetCookie = Converter.getNextStringCookie(); 97 98 // TODO: Handle resource and style references 99 if (attribute.isStyleReference()) { 100 return; 101 } 102 103 while (attribute.isResourceReference()) { 104 Integer resourceId; 105 ResName resName = attribute.getResourceReference(); 106 if (attribute.getReferenceResId() != null) { 107 resourceId = attribute.getReferenceResId(); 108 } else { 109 resourceId = resourceTable.getResourceId(resName); 110 } 111 112 if (resourceId == null) { 113 throw new Resources.NotFoundException("unknown resource " + resName); 114 } 115 outValue.type = TypedValue.TYPE_REFERENCE; 116 if (!resolveRefs) { 117 // Just return the resourceId if resolveRefs is false. 118 outValue.data = resourceId; 119 return; 120 } 121 122 outValue.resourceId = resourceId; 123 124 TypedResource dereferencedRef = resourceTable.getValue(resName, qualifiers); 125 126 if (dereferencedRef == null) { 127 Logger.strict("couldn't resolve %s from %s", resName.getFullyQualifiedName(), attribute); 128 129 if (resName.type.equals("id")) { 130 return; 131 } else if (resName.type.equals("layout")) { 132 return; // resourceId is good enough, right? 133 } else if (resName.type.equals("dimen")) { 134 return; 135 } else if (resName.type.equals("transition")) { 136 return; 137 } else if (resName.type.equals("interpolator")) { 138 return; 139 } else if (resName.type.equals("menu")) { 140 return; 141 } else if (resName.type.equals("raw")) { 142 return; 143 } else if (DrawableResourceLoader.isStillHandledHere(resName.type)) { 144 // wtf. color and drawable references reference are all kinds of stupid. 145 TypedResource drawableResource = resourceTable.getValue(resName, qualifiers); 146 if (drawableResource == null) { 147 throw new Resources.NotFoundException("can't find file for " + resName); 148 } else { 149 outValue.type = TypedValue.TYPE_STRING; 150 outValue.data = 0; 151 outValue.assetCookie = Converter.getNextStringCookie(); 152 outValue.string = (CharSequence) drawableResource.getData(); 153 return; 154 } 155 } else { 156 throw new RuntimeException("huh? " + resName); 157 } 158 } else { 159 if (dereferencedRef.isFile()) { 160 outValue.type = TypedValue.TYPE_STRING; 161 outValue.data = 0; 162 outValue.assetCookie = Converter.getNextStringCookie(); 163 outValue.string = dereferencedRef.asString(); 164 return; 165 } else if (dereferencedRef.getData() instanceof String) { 166 attribute = new AttributeResource(attribute.resName, dereferencedRef.asString(), resName.packageName); 167 if (attribute.isResourceReference()) { 168 continue; 169 } 170 if (resolveRefs) { 171 Converter.getConverter(dereferencedRef.getResType()).fillTypedValue(attribute.value, outValue); 172 return; 173 } 174 } 175 } 176 break; 177 } 178 179 if (attribute.isNull()) { 180 outValue.type = TypedValue.TYPE_NULL; 181 return; 182 } 183 184 TypedResource attrTypeData = resourceTable.getValue(attribute.resName, qualifiers); 185 if (attrTypeData != null) { 186 AttrData attrData = (AttrData) attrTypeData.getData(); 187 String format = attrData.getFormat(); 188 String[] types = format.split("\\|"); 189 for (String type : types) { 190 if ("reference".equals(type)) continue; // already handled above 191 Converter converter = Converter.getConverterFor(attrData, type); 192 193 if (converter != null) { 194 if (converter.fillTypedValue(attribute.value, outValue)) { 195 return; 196 } 197 } 198 } 199 } else { 200 /** 201 * In cases where the runtime framework doesn't know this attribute, e.g: viewportHeight (added in 21) on a 202 * KitKat runtine, then infer the attribute type from the value. 203 * 204 * TODO: When we are able to pass the SDK resources from the build environment then we can remove this 205 * and replace the NullResourceLoader with simple ResourceProvider that only parses attribute type information. 206 */ 207 ResType resType = ResType.inferFromValue(attribute.value); 208 Converter.getConverter(resType).fillTypedValue(attribute.value, outValue); 209 } 210 } 211 212 public void __constructor__() { 213 resourceTable = RuntimeEnvironment.getAppResourceTable(); 214 } 215 216 public void __constructor__(boolean isSystem) { 217 resourceTable = isSystem ? RuntimeEnvironment.getSystemResourceTable() : RuntimeEnvironment.getAppResourceTable(); 218 } 219 220 public ResourceTable getResourceTable() { 221 return resourceTable; 222 } 223 224 @HiddenApi @Implementation 225 public CharSequence getResourceText(int ident) { 226 TypedResource value = getAndResolve(ident, RuntimeEnvironment.getQualifiers(), true); 227 if (value == null) return null; 228 return (CharSequence) value.getData(); 229 } 230 231 @HiddenApi @Implementation 232 public CharSequence getResourceBagText(int ident, int bagEntryId) { 233 throw new UnsupportedOperationException(); // todo 234 } 235 236 @HiddenApi @Implementation 237 public String[] getResourceStringArray(final int id) { 238 CharSequence[] resourceTextArray = getResourceTextArray(id); 239 if (resourceTextArray == null) return null; 240 String[] strings = new String[resourceTextArray.length]; 241 for (int i = 0; i < strings.length; i++) { 242 strings[i] = resourceTextArray[i].toString(); 243 } 244 return strings; 245 } 246 247 @HiddenApi @Implementation 248 public int getResourceIdentifier(String name, String defType, String defPackage) { 249 Integer resourceId = resourceTable.getResourceId(ResName.qualifyResName(name, defPackage, defType)); 250 return resourceId == null ? 0 : resourceId; 251 } 252 253 @HiddenApi @Implementation 254 public boolean getResourceValue(int ident, int density, TypedValue outValue, boolean resolveRefs) { 255 TypedResource value = getAndResolve(ident, RuntimeEnvironment.getQualifiers(), resolveRefs); 256 if (value == null) return false; 257 258 getConverter(value).fillTypedValue(value.getData(), outValue); 259 return true; 260 } 261 262 private Converter getConverter(TypedResource value) { 263 if (value instanceof FileTypedResource.Image 264 || (value instanceof FileTypedResource 265 && ((FileTypedResource) value).getFsFile().getName().endsWith(".xml"))) { 266 return new Converter.FromFilePath(); 267 } 268 return Converter.getConverter(value.getResType()); 269 } 270 271 @HiddenApi @Implementation 272 public CharSequence[] getResourceTextArray(int resId) { 273 TypedResource value = getAndResolve(resId, RuntimeEnvironment.getQualifiers(), true); 274 if (value == null) return null; 275 List<TypedResource> items = getConverter(value).getItems(value); 276 CharSequence[] charSequences = new CharSequence[items.size()]; 277 for (int i = 0; i < items.size(); i++) { 278 TypedResource typedResource = resolve(items.get(i), RuntimeEnvironment.getQualifiers(), resId); 279 charSequences[i] = getConverter(typedResource).asCharSequence(typedResource); 280 } 281 return charSequences; 282 } 283 284 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 285 public boolean getThemeValue(int themePtr, int ident, TypedValue outValue, boolean resolveRefs) { 286 return getThemeValue((long) themePtr, ident, outValue, resolveRefs); 287 } 288 289 @HiddenApi @Implementation(minSdk = LOLLIPOP) 290 public boolean getThemeValue(long themePtr, int ident, TypedValue outValue, boolean resolveRefs) { 291 ResName resName = resourceTable.getResName(ident); 292 293 ThemeStyleSet themeStyleSet = getNativeTheme(themePtr).themeStyleSet; 294 AttributeResource attrValue = themeStyleSet.getAttrValue(resName); 295 while(attrValue != null && attrValue.isStyleReference()) { 296 ResName attrResName = attrValue.getStyleReference(); 297 if (attrValue.resName.equals(attrResName)) { 298 Logger.info("huh... circular reference for %s?", attrResName.getFullyQualifiedName()); 299 return false; 300 } 301 attrValue = themeStyleSet.getAttrValue(attrResName); 302 } 303 if (attrValue != null) { 304 convertAndFill(attrValue, outValue, RuntimeEnvironment.getQualifiers(), resolveRefs); 305 return true; 306 } 307 return false; 308 } 309 310 @HiddenApi @Implementation 311 public void ensureStringBlocks() { 312 } 313 314 @Implementation 315 public final InputStream open(String fileName) throws IOException { 316 return ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(fileName).getInputStream(); 317 } 318 319 @Implementation 320 public final InputStream open(String fileName, int accessMode) throws IOException { 321 return ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(fileName).getInputStream(); 322 } 323 324 @Implementation 325 public final AssetFileDescriptor openFd(String fileName) throws IOException { 326 File file = new File(ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(fileName).getPath()); 327 ParcelFileDescriptor parcelFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 328 return new AssetFileDescriptor(parcelFileDescriptor, 0, file.length()); 329 } 330 331 @Implementation 332 public final String[] list(String path) throws IOException { 333 FsFile file = ShadowApplication.getInstance().getAppManifest().getAssetsDirectory().join(path); 334 if (file.isDirectory()) { 335 return file.listFileNames(); 336 } 337 return new String[0]; 338 } 339 340 @HiddenApi @Implementation 341 public final InputStream openNonAsset(int cookie, String fileName, int accessMode) throws IOException { 342 final ResName resName = qualifyFromNonAssetFileName(fileName); 343 344 final FileTypedResource typedResource = 345 (FileTypedResource) resourceTable.getValue(resName, RuntimeEnvironment.getQualifiers()); 346 347 if (typedResource == null) { 348 throw new IOException("Unable to find resource for " + fileName); 349 } 350 351 if (accessMode == AssetManager.ACCESS_STREAMING) { 352 return typedResource.getFsFile().getInputStream(); 353 } else { 354 return new ByteArrayInputStream(typedResource.getFsFile().getBytes()); 355 } 356 } 357 358 private ResName qualifyFromNonAssetFileName(String fileName) { 359 if (fileName.startsWith("jar:")) { 360 // Must remove "jar:" prefix, or else qualifyFromFilePath fails on Windows 361 return ResName.qualifyFromFilePath("android", fileName.replaceFirst("jar:", "")); 362 } else { 363 return ResName.qualifyFromFilePath(ShadowApplication.getInstance().getAppManifest().getPackageName(), fileName); 364 } 365 } 366 367 @HiddenApi @Implementation 368 public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException { 369 throw new UnsupportedOperationException(); 370 } 371 372 @Implementation 373 public final XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException { 374 XmlBlock xmlBlock = XmlBlock.create(Fs.fileFromPath(fileName), "fixme"); 375 if (xmlBlock == null) { 376 throw new Resources.NotFoundException(fileName); 377 } 378 return getXmlResourceParser(null, xmlBlock, "fixme"); 379 } 380 381 public XmlResourceParser loadXmlResourceParser(int resId, String type) throws Resources.NotFoundException { 382 ResName resName = getResName(resId); 383 ResName resolvedResName = resolveResName(resName, RuntimeEnvironment.getQualifiers()); 384 if (resolvedResName == null) { 385 throw new RuntimeException("couldn't resolve " + resName.getFullyQualifiedName()); 386 } 387 resName = resolvedResName; 388 389 XmlBlock block = resourceTable.getXml(resName, RuntimeEnvironment.getQualifiers()); 390 if (block == null) { 391 throw new Resources.NotFoundException(resName.getFullyQualifiedName()); 392 } 393 394 ResourceTable resourceProvider = ResourceIds.isFrameworkResource(resId) ? RuntimeEnvironment.getSystemResourceTable() : RuntimeEnvironment.getCompileTimeResourceTable(); 395 396 return getXmlResourceParser(resourceProvider, block, resName.packageName); 397 } 398 399 private XmlResourceParser getXmlResourceParser(ResourceTable resourceProvider, XmlBlock block, String packageName) { 400 return new XmlResourceParserImpl(block.getDocument(), block.getFilename(), block.getPackageName(), 401 packageName, resourceProvider); 402 } 403 404 @HiddenApi @Implementation 405 public int addAssetPath(String path) { 406 return 1; 407 } 408 409 @HiddenApi @Implementation 410 public boolean isUpToDate() { 411 return true; 412 } 413 414 @HiddenApi @Implementation 415 public void setLocale(String locale) { 416 } 417 418 @Implementation 419 public String[] getLocales() { 420 return new String[0]; // todo 421 } 422 423 @HiddenApi @Implementation 424 public void setConfiguration(int mcc, int mnc, String locale, 425 int orientation, int touchscreen, int density, int keyboard, 426 int keyboardHidden, int navigation, int screenWidth, int screenHeight, 427 int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, 428 int screenLayout, int uiMode, int majorVersion) { 429 } 430 431 @HiddenApi @Implementation 432 public int[] getArrayIntResource(int resId) { 433 TypedResource value = getAndResolve(resId, RuntimeEnvironment.getQualifiers(), true); 434 if (value == null) return null; 435 List<TypedResource> items = getConverter(value).getItems(value); 436 int[] ints = new int[items.size()]; 437 for (int i = 0; i < items.size(); i++) { 438 TypedResource typedResource = resolve(items.get(i), RuntimeEnvironment.getQualifiers(), resId); 439 ints[i] = getConverter(typedResource).asInt(typedResource); 440 } 441 return ints; 442 } 443 444 protected TypedArray getTypedArrayResource(Resources resources, int resId) { 445 TypedResource value = getAndResolve(resId, RuntimeEnvironment.getQualifiers(), true); 446 if (value == null) { 447 return null; 448 } 449 List<TypedResource> items = getConverter(value).getItems(value); 450 return getTypedArray(resources, items, resId); 451 } 452 453 private TypedArray getTypedArray(Resources resources, List<TypedResource> typedResources, int resId) { 454 final CharSequence[] stringData = new CharSequence[typedResources.size()]; 455 final int totalLen = typedResources.size() * ShadowAssetManager.STYLE_NUM_ENTRIES; 456 final int[] data = new int[totalLen]; 457 458 for (int i = 0; i < typedResources.size(); i++) { 459 final int offset = i * ShadowAssetManager.STYLE_NUM_ENTRIES; 460 TypedResource typedResource = typedResources.get(i); 461 462 // Classify the item. 463 int type = getResourceType(typedResource); 464 if (type == -1) { 465 // This type is unsupported; leave empty. 466 continue; 467 } 468 469 final TypedValue typedValue = new TypedValue(); 470 471 if (type == TypedValue.TYPE_REFERENCE) { 472 final String reference = typedResource.asString(); 473 ResName refResName = AttributeResource.getResourceReference(reference, 474 typedResource.getXmlContext().getPackageName(), null); 475 typedValue.resourceId = resourceTable.getResourceId(refResName); 476 typedValue.data = typedValue.resourceId; 477 typedResource = resolve(typedResource, RuntimeEnvironment.getQualifiers(), typedValue.resourceId); 478 479 if (typedResource != null) { 480 // Reclassify to a non-reference type. 481 type = getResourceType(typedResource); 482 if (type == TypedValue.TYPE_ATTRIBUTE) { 483 type = TypedValue.TYPE_REFERENCE; 484 } else if (type == -1) { 485 // This type is unsupported; leave empty. 486 continue; 487 } 488 } 489 } 490 491 if (type == TypedValue.TYPE_ATTRIBUTE) { 492 final String reference = typedResource.asString(); 493 final ResName attrResName = AttributeResource.getStyleReference(reference, 494 typedResource.getXmlContext().getPackageName(), "attr"); 495 typedValue.data = resourceTable.getResourceId(attrResName); 496 } 497 498 if (typedResource != null && type != TypedValue.TYPE_NULL && type != TypedValue.TYPE_ATTRIBUTE) { 499 getConverter(typedResource).fillTypedValue(typedResource.getData(), typedValue); 500 } 501 502 data[offset + ShadowAssetManager.STYLE_TYPE] = type; 503 data[offset + ShadowAssetManager.STYLE_RESOURCE_ID] = typedValue.resourceId; 504 data[offset + ShadowAssetManager.STYLE_DATA] = typedValue.data; 505 data[offset + ShadowAssetManager.STYLE_ASSET_COOKIE] = typedValue.assetCookie; 506 data[offset + ShadowAssetManager.STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations; 507 data[offset + ShadowAssetManager.STYLE_DENSITY] = typedValue.density; 508 stringData[i] = typedResource == null ? null : typedResource.asString(); 509 } 510 511 int[] indices = new int[typedResources.size() + 1]; /* keep zeroed out */ 512 return ShadowTypedArray.create(resources, null, data, indices, typedResources.size(), stringData); 513 } 514 515 private int getResourceType(TypedResource typedResource) { 516 if (typedResource == null) { 517 return -1; 518 } 519 final ResType resType = typedResource.getResType(); 520 int type; 521 if (typedResource.getData() == null || resType == ResType.NULL) { 522 type = TypedValue.TYPE_NULL; 523 } else if (typedResource.isReference()) { 524 type = TypedValue.TYPE_REFERENCE; 525 } else if (resType == ResType.STYLE) { 526 type = TypedValue.TYPE_ATTRIBUTE; 527 } else if (resType == ResType.CHAR_SEQUENCE || resType == ResType.DRAWABLE) { 528 type = TypedValue.TYPE_STRING; 529 } else if (resType == ResType.INTEGER) { 530 type = TypedValue.TYPE_INT_DEC; 531 } else if (resType == ResType.FLOAT || resType == ResType.FRACTION) { 532 type = TypedValue.TYPE_FLOAT; 533 } else if (resType == ResType.BOOLEAN) { 534 type = TypedValue.TYPE_INT_BOOLEAN; 535 } else if (resType == ResType.DIMEN) { 536 type = TypedValue.TYPE_DIMENSION; 537 } else if (resType == ResType.COLOR) { 538 type = TypedValue.TYPE_INT_COLOR_ARGB8; 539 } else if (resType == ResType.TYPED_ARRAY || resType == ResType.CHAR_SEQUENCE_ARRAY) { 540 type = TypedValue.TYPE_REFERENCE; 541 } else { 542 type = -1; 543 } 544 return type; 545 } 546 547 @HiddenApi @Implementation 548 public Number createTheme() { 549 synchronized (nativeThemes) { 550 long nativePtr = nextInternalThemeId++; 551 nativeThemes.put(nativePtr, new NativeTheme(new ThemeStyleSet())); 552 return castNativePtr(nativePtr); 553 } 554 } 555 556 private static NativeTheme getNativeTheme(Resources.Theme theme) { 557 return getNativeTheme(shadowOf(theme).getNativePtr()); 558 } 559 560 private static NativeTheme getNativeTheme(long themePtr) { 561 NativeTheme nativeTheme; 562 synchronized (nativeThemes) { 563 nativeTheme = nativeThemes.get(themePtr); 564 } 565 if (nativeTheme == null) { 566 throw new RuntimeException("no theme " + themePtr + " found in AssetManager"); 567 } 568 return nativeTheme; 569 } 570 571 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 572 public void releaseTheme(int themePtr) { 573 releaseTheme((long) themePtr); 574 } 575 576 @HiddenApi @Implementation(minSdk = LOLLIPOP) 577 public void releaseTheme(long themePtr) { 578 synchronized (nativeThemes) { 579 nativeThemes.remove(themePtr); 580 } 581 } 582 583 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 584 public static void applyThemeStyle(int themePtr, int styleRes, boolean force) { 585 applyThemeStyle((long) themePtr, styleRes, force); 586 } 587 588 @HiddenApi @Implementation(minSdk = LOLLIPOP) 589 public static void applyThemeStyle(long themePtr, int styleRes, boolean force) { 590 NativeTheme nativeTheme = getNativeTheme(themePtr); 591 Style style = nativeTheme.getShadowAssetManager().resolveStyle(styleRes, null); 592 nativeTheme.themeStyleSet.apply(style, force); 593} 594 595 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 596 public static void copyTheme(int destPtr, int sourcePtr) { 597 copyTheme((long) destPtr, (long) sourcePtr); 598 } 599 600 @HiddenApi @Implementation(minSdk = LOLLIPOP) 601 public static void copyTheme(long destPtr, long sourcePtr) { 602 NativeTheme destNativeTheme = getNativeTheme(destPtr); 603 NativeTheme sourceNativeTheme = getNativeTheme(sourcePtr); 604 destNativeTheme.themeStyleSet = sourceNativeTheme.themeStyleSet.copy(); 605 } 606 607 ///////////////////////// 608 609 Style resolveStyle(int resId, Style themeStyleSet) { 610 return resolveStyle(getResName(resId), themeStyleSet); 611 } 612 613 private Style resolveStyle(@Nonnull ResName themeStyleName, Style themeStyleSet) { 614 TypedResource themeStyleResource = resourceTable.getValue(themeStyleName, RuntimeEnvironment.getQualifiers()); 615 if (themeStyleResource == null) return null; 616 StyleData themeStyleData = (StyleData) themeStyleResource.getData(); 617 if (themeStyleSet == null) { 618 themeStyleSet = new ThemeStyleSet(); 619 } 620 return new StyleResolver(resourceTable, shadowOf(AssetManager.getSystem()).getResourceTable(), themeStyleData, themeStyleSet, themeStyleName, RuntimeEnvironment.getQualifiers()); 621 } 622 623 private TypedResource getAndResolve(int resId, String qualifiers, boolean resolveRefs) { 624 TypedResource value = resourceTable.getValue(resId, qualifiers); 625 if (resolveRefs) { 626 value = resolve(value, qualifiers, resId); 627 } 628 return value; 629 } 630 631 TypedResource resolve(TypedResource value, String qualifiers, int resId) { 632 return resolveResourceValue(value, qualifiers, resId); 633 } 634 635 public ResName resolveResName(ResName resName, String qualifiers) { 636 TypedResource value = resourceTable.getValue(resName, qualifiers); 637 return resolveResource(value, qualifiers, resName); 638 } 639 640 // todo: DRY up #resolveResource vs #resolveResourceValue 641 private ResName resolveResource(TypedResource value, String qualifiers, ResName resName) { 642 while (value != null && value.isReference()) { 643 String s = value.asString(); 644 if (AttributeResource.isNull(s) || AttributeResource.isEmpty(s)) { 645 value = null; 646 } else { 647 String refStr = s.substring(1).replace("+", ""); 648 resName = ResName.qualifyResName(refStr, resName); 649 value = resourceTable.getValue(resName, qualifiers); 650 } 651 } 652 653 return resName; 654 } 655 656 private TypedResource resolveResourceValue(TypedResource value, String qualifiers, ResName resName) { 657 while (value != null && value.isReference()) { 658 String s = value.asString(); 659 if (AttributeResource.isNull(s) || AttributeResource.isEmpty(s)) { 660 value = null; 661 } else { 662 String refStr = s.substring(1).replace("+", ""); 663 resName = ResName.qualifyResName(refStr, resName); 664 value = resourceTable.getValue(resName, qualifiers); 665 } 666 } 667 668 return value; 669 } 670 671 public TypedResource resolveResourceValue(TypedResource value, String qualifiers, int resId) { 672 ResName resName = getResName(resId); 673 return resolveResourceValue(value, qualifiers, resName); 674 } 675 676 private TypedValue buildTypedValue(AttributeSet set, int resId, int defStyleAttr, Style themeStyleSet, int defStyleRes) { 677 /* 678 * When determining the final value of a particular attribute, there are four inputs that come into play: 679 * 680 * 1. Any attribute values in the given AttributeSet. 681 * 2. The style resource specified in the AttributeSet (named "style"). 682 * 3. The default style specified by defStyleAttr and defStyleRes 683 * 4. The base values in this theme. 684 */ 685 Style defStyleFromAttr = null; 686 Style defStyleFromRes = null; 687 Style styleAttrStyle = null; 688 689 if (defStyleAttr != 0) { 690 // Load the theme attribute for the default style attributes. E.g., attr/buttonStyle 691 ResName defStyleName = getResName(defStyleAttr); 692 693 // Load the style for the default style attribute. E.g. "@style/Widget.Robolectric.Button"; 694 AttributeResource defStyleAttribute = themeStyleSet.getAttrValue(defStyleName); 695 if (defStyleAttribute != null) { 696 while (defStyleAttribute.isStyleReference()) { 697 AttributeResource other = themeStyleSet.getAttrValue(defStyleAttribute.getStyleReference()); 698 if (other == null) { 699 throw new RuntimeException("couldn't dereference " + defStyleAttribute); 700 } 701 defStyleAttribute = other; 702 } 703 704 if (defStyleAttribute.isResourceReference()) { 705 ResName defStyleResName = defStyleAttribute.getResourceReference(); 706 defStyleFromAttr = resolveStyle(defStyleResName, themeStyleSet); 707 } 708 } 709 } 710 711 if (set != null && set.getStyleAttribute() != 0) { 712 ResName styleAttributeResName = getResName(set.getStyleAttribute()); 713 while (styleAttributeResName.type.equals("attr")) { 714 AttributeResource attrValue = themeStyleSet.getAttrValue(styleAttributeResName); 715 if (attrValue == null) { 716 throw new RuntimeException( 717 "no value for " + styleAttributeResName.getFullyQualifiedName() 718 + " in " + themeStyleSet); 719 } 720 if (attrValue.isResourceReference()) { 721 styleAttributeResName = attrValue.getResourceReference(); 722 } else if (attrValue.isStyleReference()) { 723 styleAttributeResName = attrValue.getStyleReference(); 724 } 725 } 726 styleAttrStyle = resolveStyle(styleAttributeResName, themeStyleSet); 727 } 728 729 if (defStyleRes != 0) { 730 ResName resName = getResName(defStyleRes); 731 if (resName.type.equals("attr")) { 732 AttributeResource attributeValue = findAttributeValue(defStyleRes, set, styleAttrStyle, defStyleFromAttr, defStyleFromAttr, themeStyleSet); 733 if (attributeValue != null) { 734 if (attributeValue.isStyleReference()) { 735 resName = themeStyleSet.getAttrValue(attributeValue.getStyleReference()).getResourceReference(); 736 } else if (attributeValue.isResourceReference()) { 737 resName = attributeValue.getResourceReference(); 738 } 739 } 740 } 741 defStyleFromRes = resolveStyle(resName, themeStyleSet); 742 } 743 744 AttributeResource attribute = findAttributeValue(resId, set, styleAttrStyle, defStyleFromAttr, defStyleFromRes, themeStyleSet); 745 while (attribute != null && attribute.isStyleReference()) { 746 ResName otherAttrName = attribute.getStyleReference(); 747 if (attribute.resName.equals(otherAttrName)) { 748 Logger.info("huh... circular reference for %s?", attribute.resName.getFullyQualifiedName()); 749 return null; 750 } 751 ResName resName = resourceTable.getResName(resId); 752 753 AttributeResource otherAttr = themeStyleSet.getAttrValue(otherAttrName); 754 if (otherAttr == null) { 755 strictError("no such attr %s in %s while resolving value for %s", attribute.value, themeStyleSet, resName.getFullyQualifiedName()); 756 attribute = null; 757 } else { 758 attribute = new AttributeResource(resName, otherAttr.value, otherAttr.contextPackageName); 759 } 760 } 761 762 if (attribute == null || attribute.isNull()) { 763 return null; 764 } else { 765 TypedValue typedValue = new TypedValue(); 766 convertAndFill(attribute, typedValue, RuntimeEnvironment.getQualifiers(), true); 767 return typedValue; 768 } 769 } 770 771 private void strictError(String message, Object... args) { 772 if (strictErrors) { 773 throw new RuntimeException(String.format(message, args)); 774 } else { 775 Logger.strict(message, args); 776 } 777 } 778 779 TypedArray attrsToTypedArray(Resources resources, AttributeSet set, int[] attrs, int defStyleAttr, long nativeTheme, int defStyleRes) { 780 CharSequence[] stringData = new CharSequence[attrs.length]; 781 int[] data = new int[attrs.length * ShadowAssetManager.STYLE_NUM_ENTRIES]; 782 int[] indices = new int[attrs.length + 1]; 783 int nextIndex = 0; 784 785 Style themeStyleSet = nativeTheme == 0 786 ? new EmptyStyle() 787 : getNativeTheme(nativeTheme).themeStyleSet; 788 789 for (int i = 0; i < attrs.length; i++) { 790 int offset = i * ShadowAssetManager.STYLE_NUM_ENTRIES; 791 792 TypedValue typedValue = buildTypedValue(set, attrs[i], defStyleAttr, themeStyleSet, defStyleRes); 793 if (typedValue != null) { 794 //noinspection PointlessArithmeticExpression 795 data[offset + ShadowAssetManager.STYLE_TYPE] = typedValue.type; 796 data[offset + ShadowAssetManager.STYLE_DATA] = typedValue.type == TypedValue.TYPE_STRING ? i : typedValue.data; 797 data[offset + ShadowAssetManager.STYLE_ASSET_COOKIE] = typedValue.assetCookie; 798 data[offset + ShadowAssetManager.STYLE_RESOURCE_ID] = typedValue.resourceId; 799 data[offset + ShadowAssetManager.STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations; 800 data[offset + ShadowAssetManager.STYLE_DENSITY] = typedValue.density; 801 stringData[i] = typedValue.string; 802 803 indices[nextIndex + 1] = i; 804 nextIndex++; 805 } 806 } 807 808 indices[0] = nextIndex; 809 810 TypedArray typedArray = ShadowTypedArray.create(resources, attrs, data, indices, nextIndex, stringData); 811 if (set != null) { 812 shadowOf(typedArray).positionDescription = set.getPositionDescription(); 813 } 814 return typedArray; 815 } 816 817 private AttributeResource findAttributeValue(int resId, AttributeSet attributeSet, Style styleAttrStyle, Style defStyleFromAttr, Style defStyleFromRes, @Nonnull Style themeStyleSet) { 818 if (attributeSet != null) { 819 for (int i = 0; i < attributeSet.getAttributeCount(); i++) { 820 if (attributeSet.getAttributeNameResource(i) == resId && attributeSet.getAttributeValue(i) != null) { 821 String defaultPackageName = ResourceIds.isFrameworkResource(resId) ? "android" : RuntimeEnvironment.application.getPackageName(); 822 ResName resName = ResName.qualifyResName(attributeSet.getAttributeName(i), defaultPackageName, "attr"); 823 Integer referenceResId = null; 824 if (AttributeResource.isResourceReference(attributeSet.getAttributeValue(i))) { 825 referenceResId = attributeSet.getAttributeResourceValue(i, -1); 826 } 827 return new AttributeResource(resName, attributeSet.getAttributeValue(i), "fixme!!!", referenceResId); 828 } 829 } 830 } 831 832 ResName attrName = resourceTable.getResName(resId); 833 if (attrName == null) return null; 834 835 if (styleAttrStyle != null) { 836 AttributeResource attribute = styleAttrStyle.getAttrValue(attrName); 837 if (attribute != null) { 838 return attribute; 839 } 840 } 841 842 // else if attr in defStyleFromAttr, use its value 843 if (defStyleFromAttr != null) { 844 AttributeResource attribute = defStyleFromAttr.getAttrValue(attrName); 845 if (attribute != null) { 846 return attribute; 847 } 848 } 849 850 if (defStyleFromRes != null) { 851 AttributeResource attribute = defStyleFromRes.getAttrValue(attrName); 852 if (attribute != null) { 853 return attribute; 854 } 855 } 856 857 // else if attr in theme, use its value 858 return themeStyleSet.getAttrValue(attrName); 859 } 860 861 @Nonnull private ResName getResName(int id) { 862 ResName resName = resourceTable.getResName(id); 863 if (resName == null) { 864 throw new Resources.NotFoundException("Unable to find resource ID #0x" + Integer.toHexString(id) 865 + " in packages " + resourceTable); 866 } 867 return resName; 868 } 869 870 @Implementation 871 public String getResourceName(int resid) { 872 return getResName(resid).getFullyQualifiedName(); 873 } 874 875 @Implementation 876 public String getResourcePackageName(int resid) { 877 return getResName(resid).packageName; 878 } 879 880 @Implementation 881 public String getResourceTypeName(int resid) { 882 return getResName(resid).type; 883 } 884 885 @Implementation 886 public String getResourceEntryName(int resid) { 887 return getResName(resid).name; 888 } 889 890 @Implementation 891 public final SparseArray<String> getAssignedPackageIdentifiers() { 892 return new SparseArray<>(); 893 } 894 895 @Resetter 896 public static void reset() { 897 ReflectionHelpers.setStaticField(AssetManager.class, "sSystem", null); 898 } 899} 900