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