Home | History | Annotate | Download | only in shadows
      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