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