1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.KITKAT_WATCH; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP; 5 import static android.os.Build.VERSION_CODES.O_MR1; 6 7 import static org.robolectric.RuntimeEnvironment.castNativePtr; 8 import static org.robolectric.Shadows.shadowOf; 9 import static org.robolectric.shadow.api.Shadow.directlyOn; 10 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; 11 12 import android.content.res.ApkAssets; 13 import android.content.res.AssetFileDescriptor; 14 import android.content.res.AssetManager; 15 import android.content.res.AssetManager.AssetInputStream; 16 import android.content.res.Resources; 17 import android.content.res.TypedArray; 18 import android.content.res.XmlResourceParser; 19 import android.os.Build; 20 import android.os.Build.VERSION_CODES; 21 import android.os.ParcelFileDescriptor; 22 import android.util.AttributeSet; 23 import android.util.SparseArray; 24 import android.util.TypedValue; 25 26 import com.google.common.collect.Ordering; 27 import java.io.ByteArrayInputStream; 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.FileNotFoundException; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.nio.file.Files; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Collection; 38 import java.util.Collections; 39 import java.util.HashMap; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Set; 43 import java.util.concurrent.CopyOnWriteArraySet; 44 import java.util.zip.ZipEntry; 45 import java.util.zip.ZipInputStream; 46 import javax.annotation.Nonnull; 47 import org.robolectric.RuntimeEnvironment; 48 import org.robolectric.android.XmlResourceParserImpl; 49 import org.robolectric.annotation.HiddenApi; 50 import org.robolectric.annotation.Implementation; 51 import org.robolectric.annotation.Implements; 52 import org.robolectric.annotation.RealObject; 53 import org.robolectric.annotation.Resetter; 54 import org.robolectric.res.AttrData; 55 import org.robolectric.res.AttributeResource; 56 import org.robolectric.res.EmptyStyle; 57 import org.robolectric.res.FileTypedResource; 58 import org.robolectric.res.Fs; 59 import org.robolectric.res.FsFile; 60 import org.robolectric.res.ResName; 61 import org.robolectric.res.ResType; 62 import org.robolectric.res.ResourceIds; 63 import org.robolectric.res.ResourceTable; 64 import org.robolectric.res.Style; 65 import org.robolectric.res.StyleData; 66 import org.robolectric.res.StyleResolver; 67 import org.robolectric.res.ThemeStyleSet; 68 import org.robolectric.res.TypedResource; 69 import org.robolectric.res.android.ResTable_config; 70 import org.robolectric.res.builder.XmlBlock; 71 import org.robolectric.shadow.api.Shadow; 72 import org.robolectric.util.Logger; 73 import org.robolectric.util.ReflectionHelpers; 74 75 @Implements(AssetManager.class) 76 public class ShadowAssetManager { 77 78 public static final int STYLE_NUM_ENTRIES = 6; 79 public static final int STYLE_TYPE = 0; 80 public static final int STYLE_DATA = 1; 81 public static final int STYLE_ASSET_COOKIE = 2; 82 public static final int STYLE_RESOURCE_ID = 3; 83 public static final int STYLE_CHANGING_CONFIGURATIONS = 4; 84 public static final int STYLE_DENSITY = 5; 85 86 public static final Ordering<String> ATTRIBUTE_TYPE_PRECIDENCE = 87 Ordering.explicit( 88 "reference", 89 "color", 90 "boolean", 91 "integer", 92 "fraction", 93 "dimension", 94 "float", 95 "enum", 96 "flag", 97 "string"); 98 99 boolean strictErrors = false; 100 101 private static long nextInternalThemeId = 1000; 102 private static final Map<Long, NativeTheme> nativeThemes = new HashMap<>(); 103 private ResourceTable resourceTable; 104 105 ResTable_config config = new ResTable_config(); 106 private Set<FsFile> assetDirs = new CopyOnWriteArraySet<>(); 107 108 class NativeTheme { 109 private ThemeStyleSet themeStyleSet; 110 111 public NativeTheme(ThemeStyleSet themeStyleSet) { 112 this.themeStyleSet = themeStyleSet; 113 } 114 115 public ShadowAssetManager getShadowAssetManager() { 116 return ShadowAssetManager.this; 117 } 118 } 119 120 @RealObject 121 AssetManager realObject; 122 123 private void convertAndFill(AttributeResource attribute, TypedValue outValue, ResTable_config config, boolean resolveRefs) { 124 if (attribute.isNull()) { 125 outValue.type = TypedValue.TYPE_NULL; 126 outValue.data = TypedValue.DATA_NULL_UNDEFINED; 127 return; 128 } else if (attribute.isEmpty()) { 129 outValue.type = TypedValue.TYPE_NULL; 130 outValue.data = TypedValue.DATA_NULL_EMPTY; 131 return; 132 } 133 134 // short-circuit Android caching of loaded resources cuz our string positions don't remain stable... 135 outValue.assetCookie = Converter.getNextStringCookie(); 136 outValue.changingConfigurations = 0; 137 138 // TODO: Handle resource and style references 139 if (attribute.isStyleReference()) { 140 return; 141 } 142 143 while (attribute.isResourceReference()) { 144 Integer resourceId; 145 ResName resName = attribute.getResourceReference(); 146 if (attribute.getReferenceResId() != null) { 147 resourceId = attribute.getReferenceResId(); 148 } else { 149 resourceId = resourceTable.getResourceId(resName); 150 } 151 152 if (resourceId == null) { 153 throw new Resources.NotFoundException("unknown resource " + resName); 154 } 155 outValue.type = TypedValue.TYPE_REFERENCE; 156 if (!resolveRefs) { 157 // Just return the resourceId if resolveRefs is false. 158 outValue.data = resourceId; 159 return; 160 } 161 162 outValue.resourceId = resourceId; 163 164 TypedResource dereferencedRef = resourceTable.getValue(resName, config); 165 if (dereferencedRef == null) { 166 Logger.strict("couldn't resolve %s from %s", resName.getFullyQualifiedName(), attribute); 167 return; 168 } else { 169 if (dereferencedRef.isFile()) { 170 outValue.type = TypedValue.TYPE_STRING; 171 outValue.data = 0; 172 outValue.assetCookie = Converter.getNextStringCookie(); 173 outValue.string = dereferencedRef.asString(); 174 return; 175 } else if (dereferencedRef.getData() instanceof String) { 176 attribute = new AttributeResource(attribute.resName, dereferencedRef.asString(), resName.packageName); 177 if (attribute.isResourceReference()) { 178 continue; 179 } 180 if (resolveRefs) { 181 Converter.getConverter(dereferencedRef.getResType()).fillTypedValue(attribute.value, outValue); 182 return; 183 } 184 } 185 } 186 break; 187 } 188 189 if (attribute.isNull()) { 190 outValue.type = TypedValue.TYPE_NULL; 191 return; 192 } 193 194 TypedResource attrTypeData = resourceTable.getValue(attribute.resName, config); 195 if (attrTypeData != null) { 196 AttrData attrData = (AttrData) attrTypeData.getData(); 197 String format = attrData.getFormat(); 198 String[] types = format.split("\\|"); 199 Arrays.sort(types, ATTRIBUTE_TYPE_PRECIDENCE); 200 for (String type : types) { 201 if ("reference".equals(type)) continue; // already handled above 202 Converter converter = Converter.getConverterFor(attrData, type); 203 204 if (converter != null) { 205 if (converter.fillTypedValue(attribute.value, outValue)) { 206 return; 207 } 208 } 209 } 210 } else { 211 /** 212 * In cases where the runtime framework doesn't know this attribute, e.g: viewportHeight (added in 21) on a 213 * KitKat runtine, then infer the attribute type from the value. 214 * 215 * TODO: When we are able to pass the SDK resources from the build environment then we can remove this 216 * and replace the NullResourceLoader with simple ResourceProvider that only parses attribute type information. 217 */ 218 ResType resType = ResType.inferFromValue(attribute.value); 219 Converter.getConverter(resType).fillTypedValue(attribute.value, outValue); 220 } 221 } 222 223 @Implementation 224 public void __constructor__() { 225 resourceTable = RuntimeEnvironment.getAppResourceTable(); 226 // BEGIN-INTERNAL 227 if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) { 228 Shadow.invokeConstructor(AssetManager.class, realObject); 229 } 230 // END-INTERNAL 231 } 232 233 @Implementation 234 public void __constructor__(boolean isSystem) { 235 resourceTable = isSystem ? RuntimeEnvironment.getSystemResourceTable() : RuntimeEnvironment.getAppResourceTable(); 236 // BEGIN-INTERNAL 237 if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) { 238 Shadow.invokeConstructor(AssetManager.class, realObject, from(boolean.class, isSystem)); 239 } 240 // END-INTERNAL 241 } 242 243 // BEGIN-INTERNAL 244 @Implementation(minSdk = VERSION_CODES.P) 245 protected static long nativeCreate() { 246 // Return a fake pointer, must not be 0. 247 return 1; 248 } 249 // END-INTERNAL 250 251 public ResourceTable getResourceTable() { 252 return resourceTable; 253 } 254 255 @HiddenApi @Implementation 256 public CharSequence getResourceText(int ident) { 257 TypedResource value = getAndResolve(ident, config, true); 258 if (value == null) return null; 259 return (CharSequence) value.getData(); 260 } 261 262 @HiddenApi @Implementation 263 public CharSequence getResourceBagText(int ident, int bagEntryId) { 264 throw new UnsupportedOperationException(); // todo 265 } 266 267 @HiddenApi @Implementation 268 public String[] getResourceStringArray(final int id) { 269 CharSequence[] resourceTextArray = getResourceTextArray(id); 270 if (resourceTextArray == null) return null; 271 String[] strings = new String[resourceTextArray.length]; 272 for (int i = 0; i < strings.length; i++) { 273 strings[i] = resourceTextArray[i].toString(); 274 } 275 return strings; 276 } 277 278 @HiddenApi @Implementation 279 public int getResourceIdentifier(String name, String defType, String defPackage) { 280 Integer resourceId = resourceTable.getResourceId(ResName.qualifyResName(name, defPackage, defType)); 281 return resourceId == null ? 0 : resourceId; 282 } 283 284 @HiddenApi @Implementation 285 public boolean getResourceValue(int ident, int density, TypedValue outValue, boolean resolveRefs) { 286 TypedResource value = getAndResolve(ident, config, resolveRefs); 287 if (value == null) return false; 288 289 getConverter(value).fillTypedValue(value.getData(), outValue); 290 return true; 291 } 292 293 private Converter getConverter(TypedResource value) { 294 if (value instanceof FileTypedResource.Image 295 || (value instanceof FileTypedResource 296 && ((FileTypedResource) value).getFsFile().getName().endsWith(".xml"))) { 297 return new Converter.FromFilePath(); 298 } 299 return Converter.getConverter(value.getResType()); 300 } 301 302 @HiddenApi @Implementation 303 public CharSequence[] getResourceTextArray(int resId) { 304 TypedResource value = getAndResolve(resId, config, true); 305 if (value == null) return null; 306 List<TypedResource> items = getConverter(value).getItems(value); 307 CharSequence[] charSequences = new CharSequence[items.size()]; 308 for (int i = 0; i < items.size(); i++) { 309 TypedResource typedResource = resolve(items.get(i), config, resId); 310 charSequences[i] = getConverter(typedResource).asCharSequence(typedResource); 311 } 312 return charSequences; 313 } 314 315 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 316 public boolean getThemeValue(int themePtr, int ident, TypedValue outValue, boolean resolveRefs) { 317 return getThemeValue((long) themePtr, ident, outValue, resolveRefs); 318 } 319 320 @HiddenApi @Implementation(minSdk = LOLLIPOP) 321 public boolean getThemeValue(long themePtr, int ident, TypedValue outValue, boolean resolveRefs) { 322 ResName resName = resourceTable.getResName(ident); 323 324 ThemeStyleSet themeStyleSet = getNativeTheme(themePtr).themeStyleSet; 325 AttributeResource attrValue = themeStyleSet.getAttrValue(resName); 326 while(attrValue != null && attrValue.isStyleReference()) { 327 ResName attrResName = attrValue.getStyleReference(); 328 if (attrValue.resName.equals(attrResName)) { 329 Logger.info("huh... circular reference for %s?", attrResName.getFullyQualifiedName()); 330 return false; 331 } 332 attrValue = themeStyleSet.getAttrValue(attrResName); 333 } 334 if (attrValue != null) { 335 convertAndFill(attrValue, outValue, config, resolveRefs); 336 return true; 337 } 338 return false; 339 } 340 341 @HiddenApi @Implementation 342 public void ensureStringBlocks() { 343 } 344 345 @Implementation 346 public final InputStream open(String fileName) throws IOException { 347 return findAssetFile(fileName).getInputStream(); 348 } 349 350 @Implementation 351 public final InputStream open(String fileName, int accessMode) throws IOException { 352 return findAssetFile(fileName).getInputStream(); 353 } 354 355 @Implementation 356 public final AssetFileDescriptor openFd(String fileName) throws IOException { 357 File file = new File(findAssetFile(fileName).getPath()); 358 if (file.getPath().startsWith("jar")) { 359 file = getFileFromZip(file); 360 } 361 ParcelFileDescriptor parcelFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 362 return new AssetFileDescriptor(parcelFileDescriptor, 0, file.length()); 363 } 364 365 private FsFile findAssetFile(String fileName) throws IOException { 366 for (FsFile assetDir : getAllAssetsDirectories()) { 367 FsFile assetFile = assetDir.join(fileName); 368 if (assetFile.exists()) { 369 return assetFile; 370 } 371 } 372 373 throw new FileNotFoundException("Asset file " + fileName + " not found"); 374 } 375 376 /** 377 * Extract an asset from a zipped up assets provided by the build system, this is required because there is no 378 * way to get a FileDescriptor from a zip entry. This is a temporary measure for Bazel which can be removed 379 * once binary resources are supported. 380 */ 381 private static File getFileFromZip(File file) { 382 File fileFromZip = null; 383 String pathString = file.getPath(); 384 String zipFile = pathString.substring(pathString.indexOf(":") + 1, pathString.indexOf("!")); 385 String filePathInsideZip = pathString.split("!")[1].substring(1); 386 byte[] buffer = new byte[1024]; 387 try { 388 File outputDir = Files.createTempDirectory("robolectric_assets").toFile(); 389 ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile)); 390 ZipEntry ze = zis.getNextEntry(); 391 while (ze != null) { 392 String currentFilename = ze.getName(); 393 if (!currentFilename.equals(filePathInsideZip)) { 394 ze = zis.getNextEntry(); 395 continue; 396 } 397 fileFromZip = new File(outputDir + File.separator + currentFilename); 398 new File(fileFromZip.getParent()).mkdirs(); 399 FileOutputStream fos = new FileOutputStream(fileFromZip); 400 int len; 401 while ((len = zis.read(buffer)) > 0) { 402 fos.write(buffer, 0, len); 403 } 404 fos.close(); 405 break; 406 } 407 zis.closeEntry(); 408 zis.close(); 409 } catch (IOException e) { 410 throw new RuntimeException(e); 411 } 412 return fileFromZip; 413 } 414 415 @Implementation 416 public final String[] list(String path) throws IOException { 417 List<String> assetFiles = new ArrayList<>(); 418 419 for (FsFile assetsDir : getAllAssetsDirectories()) { 420 FsFile file; 421 if (path.isEmpty()) { 422 file = assetsDir; 423 } else { 424 file = assetsDir.join(path); 425 } 426 427 if (file.isDirectory()) { 428 Collections.addAll(assetFiles, file.listFileNames()); 429 } 430 } 431 return assetFiles.toArray(new String[assetFiles.size()]); 432 } 433 434 @HiddenApi @Implementation 435 public final InputStream openNonAsset(int cookie, String fileName, int accessMode) throws IOException { 436 final ResName resName = qualifyFromNonAssetFileName(fileName); 437 438 final FileTypedResource typedResource = 439 (FileTypedResource) resourceTable.getValue(resName, config); 440 441 if (typedResource == null) { 442 throw new IOException("Unable to find resource for " + fileName); 443 } 444 445 InputStream stream; 446 if (accessMode == AssetManager.ACCESS_STREAMING) { 447 stream = typedResource.getFsFile().getInputStream(); 448 } else { 449 stream = new ByteArrayInputStream(typedResource.getFsFile().getBytes()); 450 } 451 452 // BEGIN-INTERNAL 453 if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) { 454 // Camouflage the InputStream as an AssetInputStream so subsequent instanceof checks pass. 455 AssetInputStream ais = ReflectionHelpers.callConstructor(AssetInputStream.class, 456 from(AssetManager.class, realObject), 457 from(long.class, 0)); 458 459 ShadowAssetInputStream sais = shadowOf(ais); 460 sais.setDelegate(stream); 461 sais.setNinePatch(fileName.toLowerCase().endsWith(".9.png")); 462 stream = ais; 463 } 464 // END-INTERNAL 465 466 return stream; 467 } 468 469 private ResName qualifyFromNonAssetFileName(String fileName) { 470 // Resources from a jar belong to the "android" namespace, except when they come from "resource_files.zip" 471 // when they are application resources produced by Bazel. 472 if (fileName.startsWith("jar:") && !fileName.contains("resource_files.zip")) { 473 // Must remove "jar:" prefix, or else qualifyFromFilePath fails on Windows 474 return ResName.qualifyFromFilePath("android", fileName.replaceFirst("jar:", "")); 475 } else { 476 return ResName.qualifyFromFilePath(RuntimeEnvironment.application.getPackageName(), fileName); 477 } 478 } 479 480 @HiddenApi @Implementation 481 public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException { 482 throw new UnsupportedOperationException(); 483 } 484 485 @Implementation 486 public final XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException { 487 XmlBlock xmlBlock = XmlBlock.create(Fs.fileFromPath(fileName), resourceTable.getPackageName()); 488 if (xmlBlock == null) { 489 throw new Resources.NotFoundException(fileName); 490 } 491 return getXmlResourceParser(resourceTable, xmlBlock, resourceTable.getPackageName()); 492 } 493 494 public XmlResourceParser loadXmlResourceParser(int resId, String type) throws Resources.NotFoundException { 495 ResName resName = getResName(resId); 496 ResName resolvedResName = resolveResName(resName, config); 497 if (resolvedResName == null) { 498 throw new RuntimeException("couldn't resolve " + resName.getFullyQualifiedName()); 499 } 500 resName = resolvedResName; 501 502 XmlBlock block = resourceTable.getXml(resName, config); 503 if (block == null) { 504 throw new Resources.NotFoundException(resName.getFullyQualifiedName()); 505 } 506 507 ResourceTable resourceProvider = ResourceIds.isFrameworkResource(resId) ? RuntimeEnvironment.getSystemResourceTable() : RuntimeEnvironment.getCompileTimeResourceTable(); 508 509 return getXmlResourceParser(resourceProvider, block, resName.packageName); 510 } 511 512 private XmlResourceParser getXmlResourceParser(ResourceTable resourceProvider, XmlBlock block, String packageName) { 513 return new XmlResourceParserImpl(block.getDocument(), block.getFilename(), block.getPackageName(), 514 packageName, resourceProvider); 515 } 516 517 // BEGIN-INTERNAL 518 @HiddenApi @Implementation(minSdk = VERSION_CODES.P) 519 public void setApkAssets(ApkAssets[] apkAssets, boolean invalidateCaches) { 520 for (ApkAssets apkAsset : apkAssets) { 521 assetDirs.add(Fs.newFile(apkAsset.getAssetPath())); 522 } 523 directlyOn(realObject, AssetManager.class).setApkAssets(apkAssets, invalidateCaches); 524 } 525 // END-INTERNAL 526 527 @HiddenApi @Implementation 528 public int addAssetPath(String path) { 529 assetDirs.add(Fs.newFile(path)); 530 return 1; 531 } 532 533 @HiddenApi @Implementation 534 public boolean isUpToDate() { 535 return true; 536 } 537 538 @HiddenApi @Implementation 539 public void setLocale(String locale) { 540 } 541 542 @Implementation 543 public String[] getLocales() { 544 return new String[0]; // todo 545 } 546 547 @HiddenApi @Implementation(maxSdk = VERSION_CODES.N_MR1) 548 public void setConfiguration(int mcc, int mnc, String locale, 549 int orientation, int touchscreen, int density, int keyboard, 550 int keyboardHidden, int navigation, int screenWidth, int screenHeight, 551 int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, 552 int screenLayout, int uiMode, int majorVersion) { 553 setConfiguration(mcc, mnc, locale, 554 orientation, touchscreen, density, keyboard, 555 keyboardHidden, navigation, screenWidth, screenHeight, 556 smallestScreenWidthDp, screenWidthDp, screenHeightDp, 557 screenLayout, uiMode, 0, majorVersion); 558 } 559 560 @HiddenApi @Implementation(minSdk = VERSION_CODES.O) 561 public void setConfiguration(int mcc, int mnc, String locale, 562 int orientation, int touchscreen, int density, int keyboard, 563 int keyboardHidden, int navigation, int screenWidth, int screenHeight, 564 int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, 565 int screenLayout, int uiMode, int colorMode, int majorVersion) { 566 // AssetManager* am = assetManagerForJavaObject(env, clazz); 567 568 ResTable_config config = new ResTable_config(); 569 570 // Constants duplicated from Java class android.content.res.Configuration. 571 final int kScreenLayoutRoundMask = 0x300; 572 final int kScreenLayoutRoundShift = 8; 573 574 config.mcc = mcc; 575 config.mnc = mnc; 576 config.orientation = orientation; 577 config.touchscreen = touchscreen; 578 config.density = density; 579 config.keyboard = keyboard; 580 config.inputFlags = keyboardHidden; 581 config.navigation = navigation; 582 config.screenWidth = screenWidth; 583 config.screenHeight = screenHeight; 584 config.smallestScreenWidthDp = smallestScreenWidthDp; 585 config.screenWidthDp = screenWidthDp; 586 config.screenHeightDp = screenHeightDp; 587 config.screenLayout = screenLayout; 588 config.uiMode = uiMode; 589 // config.colorMode = colorMode; // todo 590 config.sdkVersion = majorVersion; 591 config.minorVersion = 0; 592 593 // In Java, we use a 32bit integer for screenLayout, while we only use an 8bit integer 594 // in C++. We must extract the round qualifier out of the Java screenLayout and put it 595 // into screenLayout2. 596 config.screenLayout2 = 597 (byte)((screenLayout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift); 598 599 if (locale != null) { 600 config.setBcp47Locale(locale); 601 } 602 // am->setConfiguration(config, locale8); 603 604 this.config = config; 605 } 606 607 @HiddenApi @Implementation(maxSdk = O_MR1) 608 public int[] getArrayIntResource(int resId) { 609 TypedResource value = getAndResolve(resId, config, true); 610 if (value == null) return null; 611 List<TypedResource> items = getConverter(value).getItems(value); 612 int[] ints = new int[items.size()]; 613 for (int i = 0; i < items.size(); i++) { 614 TypedResource typedResource = resolve(items.get(i), config, resId); 615 ints[i] = getConverter(typedResource).asInt(typedResource); 616 } 617 return ints; 618 } 619 620 // BEGIN-INTERNAL 621 @HiddenApi @Implementation(minSdk = Build.VERSION_CODES.P) 622 protected int[] getResourceIntArray(int resId) { 623 return getArrayIntResource(resId); 624 } 625 // END-INTERNAL 626 627 protected TypedArray getTypedArrayResource(Resources resources, int resId) { 628 TypedResource value = getAndResolve(resId, config, true); 629 if (value == null) { 630 return null; 631 } 632 List<TypedResource> items = getConverter(value).getItems(value); 633 return getTypedArray(resources, items, resId); 634 } 635 636 private TypedArray getTypedArray(Resources resources, List<TypedResource> typedResources, int resId) { 637 final CharSequence[] stringData = new CharSequence[typedResources.size()]; 638 final int totalLen = typedResources.size() * ShadowAssetManager.STYLE_NUM_ENTRIES; 639 final int[] data = new int[totalLen]; 640 641 for (int i = 0; i < typedResources.size(); i++) { 642 final int offset = i * ShadowAssetManager.STYLE_NUM_ENTRIES; 643 TypedResource typedResource = typedResources.get(i); 644 645 // Classify the item. 646 int type = getResourceType(typedResource); 647 if (type == -1) { 648 // This type is unsupported; leave empty. 649 continue; 650 } 651 652 final TypedValue typedValue = new TypedValue(); 653 654 if (type == TypedValue.TYPE_REFERENCE) { 655 final String reference = typedResource.asString(); 656 ResName refResName = AttributeResource.getResourceReference(reference, 657 typedResource.getXmlContext().getPackageName(), null); 658 typedValue.resourceId = resourceTable.getResourceId(refResName); 659 typedValue.data = typedValue.resourceId; 660 typedResource = resolve(typedResource, config, typedValue.resourceId); 661 662 if (typedResource != null) { 663 // Reclassify to a non-reference type. 664 type = getResourceType(typedResource); 665 if (type == TypedValue.TYPE_ATTRIBUTE) { 666 type = TypedValue.TYPE_REFERENCE; 667 } else if (type == -1) { 668 // This type is unsupported; leave empty. 669 continue; 670 } 671 } 672 } 673 674 if (type == TypedValue.TYPE_ATTRIBUTE) { 675 final String reference = typedResource.asString(); 676 final ResName attrResName = AttributeResource.getStyleReference(reference, 677 typedResource.getXmlContext().getPackageName(), "attr"); 678 typedValue.data = resourceTable.getResourceId(attrResName); 679 } 680 681 if (typedResource != null && type != TypedValue.TYPE_NULL && type != TypedValue.TYPE_ATTRIBUTE) { 682 getConverter(typedResource).fillTypedValue(typedResource.getData(), typedValue); 683 } 684 685 data[offset + ShadowAssetManager.STYLE_TYPE] = type; 686 data[offset + ShadowAssetManager.STYLE_RESOURCE_ID] = typedValue.resourceId; 687 data[offset + ShadowAssetManager.STYLE_DATA] = typedValue.data; 688 data[offset + ShadowAssetManager.STYLE_ASSET_COOKIE] = typedValue.assetCookie; 689 data[offset + ShadowAssetManager.STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations; 690 data[offset + ShadowAssetManager.STYLE_DENSITY] = typedValue.density; 691 stringData[i] = typedResource == null ? null : typedResource.asString(); 692 } 693 694 int[] indices = new int[typedResources.size() + 1]; /* keep zeroed out */ 695 return ShadowTypedArray.create(resources, null, data, indices, typedResources.size(), stringData); 696 } 697 698 private int getResourceType(TypedResource typedResource) { 699 if (typedResource == null) { 700 return -1; 701 } 702 final ResType resType = typedResource.getResType(); 703 int type; 704 if (typedResource.getData() == null || resType == ResType.NULL) { 705 type = TypedValue.TYPE_NULL; 706 } else if (typedResource.isReference()) { 707 type = TypedValue.TYPE_REFERENCE; 708 } else if (resType == ResType.STYLE) { 709 type = TypedValue.TYPE_ATTRIBUTE; 710 } else if (resType == ResType.CHAR_SEQUENCE || resType == ResType.DRAWABLE) { 711 type = TypedValue.TYPE_STRING; 712 } else if (resType == ResType.INTEGER) { 713 type = TypedValue.TYPE_INT_DEC; 714 } else if (resType == ResType.FLOAT || resType == ResType.FRACTION) { 715 type = TypedValue.TYPE_FLOAT; 716 } else if (resType == ResType.BOOLEAN) { 717 type = TypedValue.TYPE_INT_BOOLEAN; 718 } else if (resType == ResType.DIMEN) { 719 type = TypedValue.TYPE_DIMENSION; 720 } else if (resType == ResType.COLOR) { 721 type = TypedValue.TYPE_INT_COLOR_ARGB8; 722 } else if (resType == ResType.TYPED_ARRAY || resType == ResType.CHAR_SEQUENCE_ARRAY) { 723 type = TypedValue.TYPE_REFERENCE; 724 } else { 725 type = -1; 726 } 727 return type; 728 } 729 730 @HiddenApi @Implementation 731 public Number createTheme() { 732 synchronized (nativeThemes) { 733 long nativePtr = nextInternalThemeId++; 734 nativeThemes.put(nativePtr, new NativeTheme(new ThemeStyleSet())); 735 return castNativePtr(nativePtr); 736 } 737 } 738 739 private static NativeTheme getNativeTheme(Resources.Theme theme) { 740 return getNativeTheme(shadowOf(theme).getNativePtr()); 741 } 742 743 private static NativeTheme getNativeTheme(long themePtr) { 744 NativeTheme nativeTheme; 745 synchronized (nativeThemes) { 746 nativeTheme = nativeThemes.get(themePtr); 747 } 748 if (nativeTheme == null) { 749 throw new RuntimeException("no theme " + themePtr + " found in AssetManager"); 750 } 751 return nativeTheme; 752 } 753 754 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 755 public void releaseTheme(int themePtr) { 756 releaseTheme((long) themePtr); 757 } 758 759 @HiddenApi @Implementation(minSdk = LOLLIPOP) 760 public void releaseTheme(long themePtr) { 761 synchronized (nativeThemes) { 762 nativeThemes.remove(themePtr); 763 } 764 } 765 766 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 767 public static void applyThemeStyle(int themePtr, int styleRes, boolean force) { 768 applyThemeStyle((long) themePtr, styleRes, force); 769 } 770 771 @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1) 772 public static void applyThemeStyle(long themePtr, int styleRes, boolean force) { 773 NativeTheme nativeTheme = getNativeTheme(themePtr); 774 Style style = nativeTheme.getShadowAssetManager().resolveStyle(styleRes, null); 775 nativeTheme.themeStyleSet.apply(style, force); 776 } 777 778 // BEGIN-INTERNAL 779 @HiddenApi @Implementation(minSdk = VERSION_CODES.P) 780 protected void applyStyleToTheme(long themePtr, int resId, boolean force) { 781 applyThemeStyle(themePtr, resId, force); 782 } 783 // END-INTERNAL 784 785 @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) 786 public static void copyTheme(int destPtr, int sourcePtr) { 787 copyTheme((long) destPtr, (long) sourcePtr); 788 } 789 790 @HiddenApi @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1) 791 public static void copyTheme(long destPtr, long sourcePtr) { 792 NativeTheme destNativeTheme = getNativeTheme(destPtr); 793 NativeTheme sourceNativeTheme = getNativeTheme(sourcePtr); 794 destNativeTheme.themeStyleSet = sourceNativeTheme.themeStyleSet.copy(); 795 } 796 797 // BEGIN-INTERNAL 798 @HiddenApi @Implementation(minSdk = VERSION_CODES.P) 799 protected static void nativeThemeCopy(long destPtr, long sourcePtr) { 800 copyTheme(destPtr, sourcePtr); 801 } 802 // END-INTERNAL 803 804 ///////////////////////// 805 806 Style resolveStyle(int resId, Style themeStyleSet) { 807 return resolveStyle(getResName(resId), themeStyleSet); 808 } 809 810 private Style resolveStyle(@Nonnull ResName themeStyleName, Style themeStyleSet) { 811 TypedResource themeStyleResource = resourceTable.getValue(themeStyleName, config); 812 if (themeStyleResource == null) return null; 813 StyleData themeStyleData = (StyleData) themeStyleResource.getData(); 814 if (themeStyleSet == null) { 815 themeStyleSet = new ThemeStyleSet(); 816 } 817 return new StyleResolver(resourceTable, shadowOf(AssetManager.getSystem()).getResourceTable(), 818 themeStyleData, themeStyleSet, themeStyleName, config); 819 } 820 821 private TypedResource getAndResolve(int resId, ResTable_config config, boolean resolveRefs) { 822 TypedResource value = resourceTable.getValue(resId, config); 823 if (resolveRefs) { 824 value = resolve(value, config, resId); 825 } 826 return value; 827 } 828 829 TypedResource resolve(TypedResource value, ResTable_config config, int resId) { 830 return resolveResourceValue(value, config, resId); 831 } 832 833 public ResName resolveResName(ResName resName, ResTable_config config) { 834 TypedResource value = resourceTable.getValue(resName, config); 835 return resolveResource(value, config, resName); 836 } 837 838 // todo: DRY up #resolveResource vs #resolveResourceValue 839 private ResName resolveResource(TypedResource value, ResTable_config config, ResName resName) { 840 while (value != null && value.isReference()) { 841 String s = value.asString(); 842 if (AttributeResource.isNull(s) || AttributeResource.isEmpty(s)) { 843 value = null; 844 } else { 845 String refStr = s.substring(1).replace("+", ""); 846 resName = ResName.qualifyResName(refStr, resName); 847 value = resourceTable.getValue(resName, config); 848 } 849 } 850 851 return resName; 852 } 853 854 private TypedResource resolveResourceValue(TypedResource value, ResTable_config config, ResName resName) { 855 while (value != null && value.isReference()) { 856 String s = value.asString(); 857 if (AttributeResource.isNull(s) || AttributeResource.isEmpty(s)) { 858 value = null; 859 } else { 860 String refStr = s.substring(1).replace("+", ""); 861 resName = ResName.qualifyResName(refStr, resName); 862 value = resourceTable.getValue(resName, config); 863 } 864 } 865 866 return value; 867 } 868 869 public TypedResource resolveResourceValue(TypedResource value, ResTable_config config, int resId) { 870 ResName resName = getResName(resId); 871 return resolveResourceValue(value, config, resName); 872 } 873 874 private TypedValue buildTypedValue(AttributeSet set, int resId, int defStyleAttr, Style themeStyleSet, int defStyleRes) { 875 /* 876 * When determining the final value of a particular attribute, there are four inputs that come into play: 877 * 878 * 1. Any attribute values in the given AttributeSet. 879 * 2. The style resource specified in the AttributeSet (named "style"). 880 * 3. The default style specified by defStyleAttr and defStyleRes 881 * 4. The base values in this theme. 882 */ 883 Style defStyleFromAttr = null; 884 Style defStyleFromRes = null; 885 Style styleAttrStyle = null; 886 887 if (defStyleAttr != 0) { 888 // Load the theme attribute for the default style attributes. E.g., attr/buttonStyle 889 ResName defStyleName = getResName(defStyleAttr); 890 891 // Load the style for the default style attribute. E.g. "@style/Widget.Robolectric.Button"; 892 AttributeResource defStyleAttribute = themeStyleSet.getAttrValue(defStyleName); 893 if (defStyleAttribute != null) { 894 while (defStyleAttribute.isStyleReference()) { 895 AttributeResource other = themeStyleSet.getAttrValue(defStyleAttribute.getStyleReference()); 896 if (other == null) { 897 throw new RuntimeException("couldn't dereference " + defStyleAttribute); 898 } 899 defStyleAttribute = other; 900 } 901 902 if (defStyleAttribute.isResourceReference()) { 903 ResName defStyleResName = defStyleAttribute.getResourceReference(); 904 defStyleFromAttr = resolveStyle(defStyleResName, themeStyleSet); 905 } 906 } 907 } 908 909 if (set != null && set.getStyleAttribute() != 0) { 910 ResName styleAttributeResName = getResName(set.getStyleAttribute()); 911 while (styleAttributeResName.type.equals("attr")) { 912 AttributeResource attrValue = themeStyleSet.getAttrValue(styleAttributeResName); 913 if (attrValue == null) { 914 throw new RuntimeException( 915 "no value for " + styleAttributeResName.getFullyQualifiedName() 916 + " in " + themeStyleSet); 917 } 918 if (attrValue.isResourceReference()) { 919 styleAttributeResName = attrValue.getResourceReference(); 920 } else if (attrValue.isStyleReference()) { 921 styleAttributeResName = attrValue.getStyleReference(); 922 } 923 } 924 styleAttrStyle = resolveStyle(styleAttributeResName, themeStyleSet); 925 } 926 927 if (defStyleRes != 0) { 928 ResName resName = getResName(defStyleRes); 929 if (resName.type.equals("attr")) { 930 AttributeResource attributeValue = findAttributeValue(defStyleRes, set, styleAttrStyle, defStyleFromAttr, defStyleFromAttr, themeStyleSet); 931 if (attributeValue != null) { 932 if (attributeValue.isStyleReference()) { 933 resName = themeStyleSet.getAttrValue(attributeValue.getStyleReference()).getResourceReference(); 934 } else if (attributeValue.isResourceReference()) { 935 resName = attributeValue.getResourceReference(); 936 } 937 } 938 } 939 defStyleFromRes = resolveStyle(resName, themeStyleSet); 940 } 941 942 AttributeResource attribute = findAttributeValue(resId, set, styleAttrStyle, defStyleFromAttr, defStyleFromRes, themeStyleSet); 943 while (attribute != null && attribute.isStyleReference()) { 944 ResName otherAttrName = attribute.getStyleReference(); 945 if (attribute.resName.equals(otherAttrName)) { 946 Logger.info("huh... circular reference for %s?", attribute.resName.getFullyQualifiedName()); 947 return null; 948 } 949 ResName resName = resourceTable.getResName(resId); 950 951 AttributeResource otherAttr = themeStyleSet.getAttrValue(otherAttrName); 952 if (otherAttr == null) { 953 strictError("no such attr %s in %s while resolving value for %s", attribute.value, themeStyleSet, resName.getFullyQualifiedName()); 954 attribute = null; 955 } else { 956 attribute = new AttributeResource(resName, otherAttr.value, otherAttr.contextPackageName); 957 } 958 } 959 960 if (attribute == null || attribute.isNull()) { 961 return null; 962 } else { 963 TypedValue typedValue = new TypedValue(); 964 convertAndFill(attribute, typedValue, config, true); 965 return typedValue; 966 } 967 } 968 969 private void strictError(String message, Object... args) { 970 if (strictErrors) { 971 throw new RuntimeException(String.format(message, args)); 972 } else { 973 Logger.strict(message, args); 974 } 975 } 976 977 TypedArray attrsToTypedArray(Resources resources, AttributeSet set, int[] attrs, int defStyleAttr, long nativeTheme, int defStyleRes) { 978 CharSequence[] stringData = new CharSequence[attrs.length]; 979 int[] data = new int[attrs.length * ShadowAssetManager.STYLE_NUM_ENTRIES]; 980 int[] indices = new int[attrs.length + 1]; 981 int nextIndex = 0; 982 983 Style themeStyleSet = nativeTheme == 0 984 ? new EmptyStyle() 985 : getNativeTheme(nativeTheme).themeStyleSet; 986 987 for (int i = 0; i < attrs.length; i++) { 988 int offset = i * ShadowAssetManager.STYLE_NUM_ENTRIES; 989 990 TypedValue typedValue = buildTypedValue(set, attrs[i], defStyleAttr, themeStyleSet, defStyleRes); 991 if (typedValue != null) { 992 //noinspection PointlessArithmeticExpression 993 data[offset + ShadowAssetManager.STYLE_TYPE] = typedValue.type; 994 data[offset + ShadowAssetManager.STYLE_DATA] = typedValue.type == TypedValue.TYPE_STRING ? i : typedValue.data; 995 data[offset + ShadowAssetManager.STYLE_ASSET_COOKIE] = typedValue.assetCookie; 996 data[offset + ShadowAssetManager.STYLE_RESOURCE_ID] = typedValue.resourceId; 997 data[offset + ShadowAssetManager.STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations; 998 data[offset + ShadowAssetManager.STYLE_DENSITY] = typedValue.density; 999 stringData[i] = typedValue.string; 1000 1001 indices[nextIndex + 1] = i; 1002 nextIndex++; 1003 } 1004 } 1005 1006 indices[0] = nextIndex; 1007 1008 TypedArray typedArray = ShadowTypedArray.create(resources, attrs, data, indices, nextIndex, stringData); 1009 if (set != null) { 1010 shadowOf(typedArray).positionDescription = set.getPositionDescription(); 1011 } 1012 return typedArray; 1013 } 1014 1015 private AttributeResource findAttributeValue(int resId, AttributeSet attributeSet, Style styleAttrStyle, Style defStyleFromAttr, Style defStyleFromRes, @Nonnull Style themeStyleSet) { 1016 if (attributeSet != null) { 1017 for (int i = 0; i < attributeSet.getAttributeCount(); i++) { 1018 if (attributeSet.getAttributeNameResource(i) == resId && attributeSet.getAttributeValue(i) != null) { 1019 String defaultPackageName = ResourceIds.isFrameworkResource(resId) ? "android" : RuntimeEnvironment.application.getPackageName(); 1020 ResName resName = ResName.qualifyResName(attributeSet.getAttributeName(i), defaultPackageName, "attr"); 1021 Integer referenceResId = null; 1022 if (AttributeResource.isResourceReference(attributeSet.getAttributeValue(i))) { 1023 referenceResId = attributeSet.getAttributeResourceValue(i, -1); 1024 } 1025 return new AttributeResource(resName, attributeSet.getAttributeValue(i), "fixme!!!", referenceResId); 1026 } 1027 } 1028 } 1029 1030 ResName attrName = resourceTable.getResName(resId); 1031 if (attrName == null) return null; 1032 1033 if (styleAttrStyle != null) { 1034 AttributeResource attribute = styleAttrStyle.getAttrValue(attrName); 1035 if (attribute != null) { 1036 return attribute; 1037 } 1038 } 1039 1040 // else if attr in defStyleFromAttr, use its value 1041 if (defStyleFromAttr != null) { 1042 AttributeResource attribute = defStyleFromAttr.getAttrValue(attrName); 1043 if (attribute != null) { 1044 return attribute; 1045 } 1046 } 1047 1048 if (defStyleFromRes != null) { 1049 AttributeResource attribute = defStyleFromRes.getAttrValue(attrName); 1050 if (attribute != null) { 1051 return attribute; 1052 } 1053 } 1054 1055 // else if attr in theme, use its value 1056 return themeStyleSet.getAttrValue(attrName); 1057 } 1058 1059 Collection<FsFile> getAllAssetsDirectories() { 1060 return assetDirs; 1061 } 1062 1063 @Nonnull private ResName getResName(int id) { 1064 ResName resName = resourceTable.getResName(id); 1065 if (resName == null) { 1066 throw new Resources.NotFoundException("Unable to find resource ID #0x" + Integer.toHexString(id) 1067 + " in packages " + resourceTable); 1068 } 1069 return resName; 1070 } 1071 1072 @Implementation 1073 public String getResourceName(int resid) { 1074 return getResName(resid).getFullyQualifiedName(); 1075 } 1076 1077 @Implementation 1078 public String getResourcePackageName(int resid) { 1079 return getResName(resid).packageName; 1080 } 1081 1082 @Implementation 1083 public String getResourceTypeName(int resid) { 1084 return getResName(resid).type; 1085 } 1086 1087 @Implementation 1088 public String getResourceEntryName(int resid) { 1089 return getResName(resid).name; 1090 } 1091 1092 @Implementation 1093 public final SparseArray<String> getAssignedPackageIdentifiers() { 1094 return new SparseArray<>(); 1095 } 1096 1097 @Resetter 1098 public static void reset() { 1099 ReflectionHelpers.setStaticField(AssetManager.class, "sSystem", null); 1100 } 1101 } 1102