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