1package org.robolectric.shadows;
2
3import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
4import static android.os.Build.VERSION_CODES.LOLLIPOP;
5import static android.os.Build.VERSION_CODES.O_MR1;
6
7import static org.robolectric.RuntimeEnvironment.castNativePtr;
8import static org.robolectric.Shadows.shadowOf;
9import static org.robolectric.shadow.api.Shadow.directlyOn;
10import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
11
12import android.content.res.ApkAssets;
13import android.content.res.AssetFileDescriptor;
14import android.content.res.AssetManager;
15import android.content.res.AssetManager.AssetInputStream;
16import android.content.res.Resources;
17import android.content.res.TypedArray;
18import android.content.res.XmlResourceParser;
19import android.os.Build;
20import android.os.Build.VERSION_CODES;
21import android.os.ParcelFileDescriptor;
22import android.util.AttributeSet;
23import android.util.SparseArray;
24import android.util.TypedValue;
25
26import com.google.common.collect.Ordering;
27import java.io.ByteArrayInputStream;
28import java.io.File;
29import java.io.FileInputStream;
30import java.io.FileNotFoundException;
31import java.io.FileOutputStream;
32import java.io.IOException;
33import java.io.InputStream;
34import java.nio.file.Files;
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.Collection;
38import java.util.Collections;
39import java.util.HashMap;
40import java.util.List;
41import java.util.Map;
42import java.util.Set;
43import java.util.concurrent.CopyOnWriteArraySet;
44import java.util.zip.ZipEntry;
45import java.util.zip.ZipInputStream;
46import javax.annotation.Nonnull;
47import org.robolectric.RuntimeEnvironment;
48import org.robolectric.android.XmlResourceParserImpl;
49import org.robolectric.annotation.HiddenApi;
50import org.robolectric.annotation.Implementation;
51import org.robolectric.annotation.Implements;
52import org.robolectric.annotation.RealObject;
53import org.robolectric.annotation.Resetter;
54import org.robolectric.res.AttrData;
55import org.robolectric.res.AttributeResource;
56import org.robolectric.res.EmptyStyle;
57import org.robolectric.res.FileTypedResource;
58import org.robolectric.res.Fs;
59import org.robolectric.res.FsFile;
60import org.robolectric.res.ResName;
61import org.robolectric.res.ResType;
62import org.robolectric.res.ResourceIds;
63import org.robolectric.res.ResourceTable;
64import org.robolectric.res.Style;
65import org.robolectric.res.StyleData;
66import org.robolectric.res.StyleResolver;
67import org.robolectric.res.ThemeStyleSet;
68import org.robolectric.res.TypedResource;
69import org.robolectric.res.android.ResTable_config;
70import org.robolectric.res.builder.XmlBlock;
71import org.robolectric.shadow.api.Shadow;
72import org.robolectric.util.Logger;
73import org.robolectric.util.ReflectionHelpers;
74
75@Implements(AssetManager.class)
76public 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