• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
4 import static android.os.Build.VERSION_CODES.LOLLIPOP;
5 import static android.os.Build.VERSION_CODES.M;
6 import static android.os.Build.VERSION_CODES.N;
7 import static android.os.Build.VERSION_CODES.N_MR1;
8 import static org.robolectric.shadow.api.Shadow.directlyOn;
9 import static org.robolectric.shadows.ShadowAssetManager.legacyShadowOf;
10 
11 import android.content.res.AssetFileDescriptor;
12 import android.content.res.AssetManager;
13 import android.content.res.Configuration;
14 import android.content.res.Resources;
15 import android.content.res.Resources.NotFoundException;
16 import android.content.res.ResourcesImpl;
17 import android.content.res.TypedArray;
18 import android.content.res.XmlResourceParser;
19 import android.graphics.Bitmap;
20 import android.graphics.drawable.BitmapDrawable;
21 import android.graphics.drawable.Drawable;
22 import android.os.ParcelFileDescriptor;
23 import android.util.AttributeSet;
24 import android.util.DisplayMetrics;
25 import android.util.LongSparseArray;
26 import android.util.TypedValue;
27 import java.io.FileInputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.Modifier;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.Locale;
35 import org.robolectric.RuntimeEnvironment;
36 import org.robolectric.annotation.HiddenApi;
37 import org.robolectric.annotation.Implementation;
38 import org.robolectric.annotation.Implements;
39 import org.robolectric.annotation.RealObject;
40 import org.robolectric.annotation.Resetter;
41 import org.robolectric.res.Plural;
42 import org.robolectric.res.PluralRules;
43 import org.robolectric.res.ResName;
44 import org.robolectric.res.ResType;
45 import org.robolectric.res.ResourceTable;
46 import org.robolectric.res.TypedResource;
47 import org.robolectric.shadow.api.Shadow;
48 import org.robolectric.shadows.ShadowLegacyResourcesImpl.ShadowLegacyThemeImpl;
49 import org.robolectric.util.ReflectionHelpers;
50 import org.robolectric.util.ReflectionHelpers.ClassParameter;
51 
52 @Implements(Resources.class)
53 public class ShadowResources {
54 
55   private static Resources system = null;
56   private static List<LongSparseArray<?>> resettableArrays;
57 
58   @RealObject Resources realResources;
59 
60   @Resetter
reset()61   public static void reset() {
62     if (resettableArrays == null) {
63       resettableArrays = obtainResettableArrays();
64     }
65     for (LongSparseArray<?> sparseArray : resettableArrays) {
66       sparseArray.clear();
67     }
68     system = null;
69 
70     ReflectionHelpers.setStaticField(Resources.class, "mSystem", null);
71   }
72 
73   @Implementation
getSystem()74   protected static Resources getSystem() {
75     if (system == null) {
76       AssetManager assetManager = AssetManager.getSystem();
77       DisplayMetrics metrics = new DisplayMetrics();
78       Configuration config = new Configuration();
79       system = new Resources(assetManager, metrics, config);
80     }
81     return system;
82   }
83 
84   @Implementation
obtainAttributes(AttributeSet set, int[] attrs)85   protected TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
86     if (isLegacyAssetManager()) {
87       return legacyShadowOf(realResources.getAssets())
88           .attrsToTypedArray(realResources, set, attrs, 0, 0, 0);
89     } else {
90       return directlyOn(realResources, Resources.class).obtainAttributes(set, attrs);
91     }
92   }
93 
94   @Implementation
getQuantityString(int id, int quantity, Object... formatArgs)95   protected String getQuantityString(int id, int quantity, Object... formatArgs)
96       throws Resources.NotFoundException {
97     if (isLegacyAssetManager()) {
98       String raw = getQuantityString(id, quantity);
99       return String.format(Locale.ENGLISH, raw, formatArgs);
100     } else {
101       return directlyOn(realResources, Resources.class).getQuantityString(id, quantity, formatArgs);
102     }
103   }
104 
105   @Implementation
getQuantityString(int resId, int quantity)106   protected String getQuantityString(int resId, int quantity) throws Resources.NotFoundException {
107     if (isLegacyAssetManager()) {
108       ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
109 
110       TypedResource typedResource = shadowAssetManager.getResourceTable()
111           .getValue(resId, shadowAssetManager.config);
112       if (typedResource != null && typedResource instanceof PluralRules) {
113         PluralRules pluralRules = (PluralRules) typedResource;
114         Plural plural = pluralRules.find(quantity);
115 
116         if (plural == null) {
117           return null;
118         }
119 
120         TypedResource<?> resolvedTypedResource = shadowAssetManager.resolve(
121             new TypedResource<>(plural.getString(), ResType.CHAR_SEQUENCE, pluralRules.getXmlContext()),
122             shadowAssetManager.config, resId);
123         return resolvedTypedResource == null ? null : resolvedTypedResource.asString();
124       } else {
125         return null;
126       }
127     } else {
128       return directlyOn(realResources, Resources.class).getQuantityString(resId, quantity);
129     }
130   }
131 
132   @Implementation
openRawResource(int id)133   protected InputStream openRawResource(int id) throws Resources.NotFoundException {
134     if (isLegacyAssetManager()) {
135       ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
136       ResourceTable resourceTable = shadowAssetManager.getResourceTable();
137       InputStream inputStream = resourceTable.getRawValue(id, shadowAssetManager.config);
138       if (inputStream == null) {
139         throw newNotFoundException(id);
140       } else {
141         return inputStream;
142       }
143     } else {
144       return directlyOn(realResources, Resources.class).openRawResource(id);
145     }
146   }
147 
148   /**
149    * Since {@link AssetFileDescriptor}s are not yet supported by Robolectric, {@code null} will be
150    * returned if the resource is found. If the resource cannot be found, {@link
151    * Resources.NotFoundException} will be thrown.
152    */
153   @Implementation
openRawResourceFd(int id)154   protected AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
155     if (isLegacyAssetManager()) {
156       InputStream inputStream = openRawResource(id);
157       if (!(inputStream instanceof FileInputStream)) {
158         // todo fixme
159         return null;
160       }
161 
162       FileInputStream fis = (FileInputStream) inputStream;
163       try {
164         return new AssetFileDescriptor(ParcelFileDescriptor.dup(fis.getFD()), 0,
165             fis.getChannel().size());
166       } catch (IOException e) {
167         throw newNotFoundException(id);
168       }
169     } else {
170       return directlyOn(realResources, Resources.class).openRawResourceFd(id);
171     }
172   }
173 
newNotFoundException(int id)174   private Resources.NotFoundException newNotFoundException(int id) {
175     ResourceTable resourceTable = legacyShadowOf(realResources.getAssets()).getResourceTable();
176     ResName resName = resourceTable.getResName(id);
177     if (resName == null) {
178       return new Resources.NotFoundException("resource ID #0x" + Integer.toHexString(id));
179     } else {
180       return new Resources.NotFoundException(resName.getFullyQualifiedName());
181     }
182   }
183 
184   @Implementation
obtainTypedArray(int id)185   protected TypedArray obtainTypedArray(int id) throws Resources.NotFoundException {
186     if (isLegacyAssetManager()) {
187       ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
188       TypedArray typedArray = shadowAssetManager.getTypedArrayResource(realResources, id);
189       if (typedArray != null) {
190         return typedArray;
191       } else {
192         throw newNotFoundException(id);
193       }
194     } else {
195       return directlyOn(realResources, Resources.class).obtainTypedArray(id);
196     }
197   }
198 
199   @HiddenApi
200   @Implementation
loadXmlResourceParser(int resId, String type)201   protected XmlResourceParser loadXmlResourceParser(int resId, String type)
202       throws Resources.NotFoundException {
203     if (isLegacyAssetManager()) {
204       ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
205       return shadowAssetManager.loadXmlResourceParser(resId, type);
206     } else {
207       return directlyOn(realResources, Resources.class, "loadXmlResourceParser",
208           ClassParameter.from(int.class, resId),
209           ClassParameter.from(String.class, type));
210     }
211   }
212 
213   @HiddenApi
214   @Implementation
loadXmlResourceParser( String file, int id, int assetCookie, String type)215   protected XmlResourceParser loadXmlResourceParser(
216       String file, int id, int assetCookie, String type) throws Resources.NotFoundException {
217     if (isLegacyAssetManager()) {
218       return loadXmlResourceParser(id, type);
219     } else {
220       return directlyOn(realResources, Resources.class, "loadXmlResourceParser",
221           ClassParameter.from(String.class, file),
222           ClassParameter.from(int.class, id),
223           ClassParameter.from(int.class, assetCookie),
224           ClassParameter.from(String.class, type));
225     }
226   }
227 
228   @HiddenApi
229   @Implementation(maxSdk = KITKAT_WATCH)
loadDrawable(TypedValue value, int id)230   protected Drawable loadDrawable(TypedValue value, int id) {
231     Drawable drawable = directlyOn(realResources, Resources.class, "loadDrawable",
232         ClassParameter.from(TypedValue.class, value),
233         ClassParameter.from(int.class, id));
234     setCreatedFromResId(realResources, id, drawable);
235     return drawable;
236   }
237 
238   @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
loadDrawable(TypedValue value, int id, Resources.Theme theme)239   protected Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme)
240       throws Resources.NotFoundException {
241     Drawable drawable = directlyOn(realResources, Resources.class, "loadDrawable",
242         ClassParameter.from(TypedValue.class, value), ClassParameter.from(int.class, id), ClassParameter.from(Resources.Theme.class, theme));
243     setCreatedFromResId(realResources, id, drawable);
244     return drawable;
245   }
246 
obtainResettableArrays()247   private static List<LongSparseArray<?>> obtainResettableArrays() {
248     List<LongSparseArray<?>> resettableArrays = new ArrayList<>();
249     Field[] allFields = Resources.class.getDeclaredFields();
250     for (Field field : allFields) {
251       if (Modifier.isStatic(field.getModifiers()) && field.getType().equals(LongSparseArray.class)) {
252         field.setAccessible(true);
253         try {
254           LongSparseArray<?> longSparseArray = (LongSparseArray<?>) field.get(null);
255           if (longSparseArray != null) {
256             resettableArrays.add(longSparseArray);
257           }
258         } catch (IllegalAccessException e) {
259           throw new RuntimeException(e);
260         }
261       }
262     }
263     return resettableArrays;
264   }
265 
266   public static abstract class ShadowTheme {
267 
268     public static class Picker extends ResourceModeShadowPicker<ShadowTheme> {
269 
Picker()270       public Picker() {
271         super(ShadowLegacyTheme.class, null, null);
272       }
273     }
274   }
275 
276   @Implements(value = Resources.Theme.class, shadowPicker = ShadowTheme.Picker.class)
277   public static class ShadowLegacyTheme extends ShadowTheme {
278     @RealObject Resources.Theme realTheme;
279 
getNativePtr()280     long getNativePtr() {
281       if (RuntimeEnvironment.getApiLevel() >= N) {
282         ResourcesImpl.ThemeImpl themeImpl = ReflectionHelpers.getField(realTheme, "mThemeImpl");
283         return ((ShadowLegacyThemeImpl) Shadow.extract(themeImpl)).getNativePtr();
284       } else {
285         return ((Number) ReflectionHelpers.getField(realTheme, "mTheme")).longValue();
286       }
287     }
288 
289     @Implementation(maxSdk = M)
obtainStyledAttributes(int[] attrs)290     protected TypedArray obtainStyledAttributes(int[] attrs) {
291       return obtainStyledAttributes(0, attrs);
292     }
293 
294     @Implementation(maxSdk = M)
obtainStyledAttributes(int resid, int[] attrs)295     protected TypedArray obtainStyledAttributes(int resid, int[] attrs)
296         throws Resources.NotFoundException {
297       return obtainStyledAttributes(null, attrs, 0, resid);
298     }
299 
300     @Implementation(maxSdk = M)
obtainStyledAttributes( AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)301     protected TypedArray obtainStyledAttributes(
302         AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
303       return getShadowAssetManager().attrsToTypedArray(getResources(), set, attrs, defStyleAttr, getNativePtr(), defStyleRes);
304     }
305 
getShadowAssetManager()306     private ShadowLegacyAssetManager getShadowAssetManager() {
307       return legacyShadowOf(getResources().getAssets());
308     }
309 
getResources()310     private Resources getResources() {
311       return ReflectionHelpers.getField(realTheme, "this$0");
312     }
313   }
314 
315 
setCreatedFromResId(Resources resources, int id, Drawable drawable)316   static void setCreatedFromResId(Resources resources, int id, Drawable drawable) {
317     // todo: this kinda sucks, find some better way...
318     if (drawable != null && Shadow.extract(drawable) instanceof ShadowDrawable) {
319       ShadowDrawable shadowDrawable = Shadow.extract(drawable);
320       shadowDrawable.createdFromResId = id;
321       if (drawable instanceof BitmapDrawable) {
322         Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
323         if (bitmap != null  && Shadow.extract(bitmap) instanceof ShadowBitmap) {
324           ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
325           if (shadowBitmap.createdFromResId == -1) {
326             String resourceName;
327             try {
328               resourceName = resources.getResourceName(id);
329             } catch (NotFoundException e) {
330               resourceName = "Unknown resource #0x" + Integer.toHexString(id);
331             }
332             shadowBitmap.setCreatedFromResId(id, resourceName);
333           }
334         }
335       }
336     }
337   }
338 
isLegacyAssetManager()339   private boolean isLegacyAssetManager() {
340     return ShadowAssetManager.useLegacy();
341   }
342 
343   @Implements(Resources.NotFoundException.class)
344   public static class ShadowNotFoundException {
345     @RealObject Resources.NotFoundException realObject;
346 
347     private String message;
348 
349     @Implementation
__constructor__()350     protected void __constructor__() {}
351 
352     @Implementation
__constructor__(String name)353     protected void __constructor__(String name) {
354       this.message = name;
355     }
356 
357     @Override @Implementation
toString()358     public String toString() {
359       return realObject.getClass().getName() + ": " + message;
360     }
361   }
362 }
363