• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.N_MR1;
4 import static android.os.Build.VERSION_CODES.Q;
5 import static org.robolectric.util.reflector.Reflector.reflector;
6 
7 import android.content.res.AssetFileDescriptor;
8 import android.content.res.AssetManager;
9 import android.content.res.CompatibilityInfo;
10 import android.content.res.Configuration;
11 import android.content.res.Resources;
12 import android.content.res.Resources.NotFoundException;
13 import android.content.res.TypedArray;
14 import android.content.res.XmlResourceParser;
15 import android.graphics.drawable.Drawable;
16 import android.util.AttributeSet;
17 import android.util.DisplayMetrics;
18 import android.util.LongSparseArray;
19 import android.util.TypedValue;
20 import java.io.InputStream;
21 import java.lang.reflect.Field;
22 import java.lang.reflect.Modifier;
23 import java.util.ArrayList;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Set;
27 import org.robolectric.RuntimeEnvironment;
28 import org.robolectric.android.Bootstrap;
29 import org.robolectric.annotation.HiddenApi;
30 import org.robolectric.annotation.Implementation;
31 import org.robolectric.annotation.Implements;
32 import org.robolectric.annotation.RealObject;
33 import org.robolectric.annotation.Resetter;
34 import org.robolectric.internal.bytecode.ShadowedObject;
35 import org.robolectric.shadow.api.Shadow;
36 import org.robolectric.util.ReflectionHelpers;
37 import org.robolectric.util.reflector.Direct;
38 import org.robolectric.util.reflector.ForType;
39 
40 /** Shadow of {@link Resources}. */
41 @Implements(Resources.class)
42 public class ShadowResources {
43 
44   private static Resources system = null;
45   private static List<LongSparseArray<?>> resettableArrays;
46 
47   @RealObject Resources realResources;
48   private final Set<OnConfigurationChangeListener> configurationChangeListeners = new HashSet<>();
49 
50   @Resetter
reset()51   public static void reset() {
52     if (resettableArrays == null) {
53       resettableArrays = obtainResettableArrays();
54     }
55     for (LongSparseArray<?> sparseArray : resettableArrays) {
56       sparseArray.clear();
57     }
58     system = null;
59 
60     ReflectionHelpers.setStaticField(Resources.class, "mSystem", null);
61   }
62 
63   @Implementation
getSystem()64   protected static Resources getSystem() {
65     if (system == null) {
66       AssetManager assetManager = AssetManager.getSystem();
67       system =
68           new Resources(assetManager, Bootstrap.getDisplayMetrics(), Bootstrap.getConfiguration());
69     }
70     return system;
71   }
72 
73   @HiddenApi
74   @Implementation
loadXmlResourceParser(int resId, String type)75   protected XmlResourceParser loadXmlResourceParser(int resId, String type)
76       throws Resources.NotFoundException {
77 
78     ResourcesReflector relectedResources = reflector(ResourcesReflector.class, realResources);
79     return setSourceResourceId(relectedResources.loadXmlResourceParser(resId, type), resId);
80   }
81 
82   @HiddenApi
83   @Implementation
loadXmlResourceParser( String file, int id, int assetCookie, String type)84   protected XmlResourceParser loadXmlResourceParser(
85       String file, int id, int assetCookie, String type) throws Resources.NotFoundException {
86 
87     ResourcesReflector relectedResources = reflector(ResourcesReflector.class, realResources);
88     return setSourceResourceId(
89         relectedResources.loadXmlResourceParser(file, id, assetCookie, type), id);
90   }
91 
setSourceResourceId(XmlResourceParser parser, int resourceId)92   private static XmlResourceParser setSourceResourceId(XmlResourceParser parser, int resourceId) {
93     Object shadow = parser instanceof ShadowedObject ? Shadow.extract(parser) : null;
94     if (shadow instanceof ShadowXmlBlock.ShadowParser) {
95       ((ShadowXmlBlock.ShadowParser) shadow).setSourceResourceId(resourceId);
96     }
97     return parser;
98   }
99 
100   @Implementation(maxSdk = N_MR1)
loadDrawable(TypedValue value, int id, Resources.Theme theme)101   protected Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme)
102       throws Resources.NotFoundException {
103     Drawable drawable =
104         reflector(ResourcesReflector.class, realResources).loadDrawable(value, id, theme);
105     setCreatedFromResId(realResources, id, drawable);
106     return drawable;
107   }
108 
obtainResettableArrays()109   private static List<LongSparseArray<?>> obtainResettableArrays() {
110     List<LongSparseArray<?>> resettableArrays = new ArrayList<>();
111     Field[] allFields = Resources.class.getDeclaredFields();
112     for (Field field : allFields) {
113       if (Modifier.isStatic(field.getModifiers())
114           && field.getType().equals(LongSparseArray.class)) {
115         field.setAccessible(true);
116         try {
117           LongSparseArray<?> longSparseArray = (LongSparseArray<?>) field.get(null);
118           if (longSparseArray != null) {
119             resettableArrays.add(longSparseArray);
120           }
121         } catch (IllegalAccessException e) {
122           throw new RuntimeException(e);
123         }
124       }
125     }
126     return resettableArrays;
127   }
128 
129   /**
130    * Returns the layout resource id the attribute set was inflated from. Backwards compatible
131    * version of {@link Resources#getAttributeSetSourceResId(AttributeSet)}, passes through to the
132    * underlying implementation on API levels where it is supported.
133    */
134   @Implementation(minSdk = Q)
getAttributeSetSourceResId(AttributeSet attrs)135   public static int getAttributeSetSourceResId(AttributeSet attrs) {
136     if (RuntimeEnvironment.getApiLevel() >= Q) {
137       return reflector(ResourcesReflector.class).getAttributeSetSourceResId(attrs);
138     } else {
139       Object shadow = attrs instanceof ShadowedObject ? Shadow.extract(attrs) : null;
140       return shadow instanceof ShadowXmlBlock.ShadowParser
141           ? ((ShadowXmlBlock.ShadowParser) shadow).getSourceResourceId()
142           : 0;
143     }
144   }
145 
146   /**
147    * Listener callback that's called when the configuration is updated for a resources. The callback
148    * receives the old and new configs (and can use {@link Configuration#diff(Configuration)} to
149    * produce a diff). The callback is called after the configuration has been applied to the
150    * underlying resources, so obtaining resources will use the new configuration in the callback.
151    */
152   public interface OnConfigurationChangeListener {
onConfigurationChange( Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics)153     void onConfigurationChange(
154         Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics);
155   }
156 
157   /**
158    * Add a listener to observe resource configuration changes. See {@link
159    * OnConfigurationChangeListener}.
160    */
addConfigurationChangeListener(OnConfigurationChangeListener listener)161   public void addConfigurationChangeListener(OnConfigurationChangeListener listener) {
162     configurationChangeListeners.add(listener);
163   }
164 
165   /**
166    * Remove a listener to observe resource configuration changes. See {@link
167    * OnConfigurationChangeListener}.
168    */
removeConfigurationChangeListener(OnConfigurationChangeListener listener)169   public void removeConfigurationChangeListener(OnConfigurationChangeListener listener) {
170     configurationChangeListeners.remove(listener);
171   }
172 
173   @Implementation
updateConfiguration( Configuration config, DisplayMetrics metrics, CompatibilityInfo compat)174   protected void updateConfiguration(
175       Configuration config, DisplayMetrics metrics, CompatibilityInfo compat) {
176     Configuration oldConfig;
177     try {
178       oldConfig = new Configuration(realResources.getConfiguration());
179     } catch (NullPointerException e) {
180       // In old versions of Android the resource constructor calls updateConfiguration, in the
181       // app compat ResourcesWrapper subclass the reference to the underlying resources hasn't been
182       // configured yet, so it'll throw an NPE, catch this to avoid crashing.
183       oldConfig = null;
184     }
185     reflector(ResourcesReflector.class, realResources).updateConfiguration(config, metrics, compat);
186     if (oldConfig != null && config != null) {
187       for (OnConfigurationChangeListener listener : configurationChangeListeners) {
188         listener.onConfigurationChange(oldConfig, config, metrics);
189       }
190     }
191   }
192 
setCreatedFromResId(Resources resources, int id, Drawable drawable)193   static void setCreatedFromResId(Resources resources, int id, Drawable drawable) {
194     // todo: this kinda sucks, find some better way...
195     if (drawable != null && Shadow.extract(drawable) instanceof ShadowDrawable) {
196       ShadowDrawable shadowDrawable = Shadow.extract(drawable);
197 
198       String resourceName;
199       try {
200         resourceName = resources.getResourceName(id);
201       } catch (NotFoundException e) {
202         resourceName = "Unknown resource #0x" + Integer.toHexString(id);
203       }
204 
205       shadowDrawable.setCreatedFromResId(id, resourceName);
206     }
207   }
208 
209   /** Shadow for {@link Resources.NotFoundException}. */
210   @Implements(Resources.NotFoundException.class)
211   public static class ShadowNotFoundException {
212     @RealObject Resources.NotFoundException realObject;
213 
214     private String message;
215 
216     @Implementation
__constructor__()217     protected void __constructor__() {}
218 
219     @Implementation
__constructor__(String name)220     protected void __constructor__(String name) {
221       this.message = name;
222     }
223 
224     @Override
225     @Implementation
toString()226     public String toString() {
227       return realObject.getClass().getName() + ": " + message;
228     }
229   }
230 
231   @ForType(Resources.class)
232   interface ResourcesReflector {
233 
234     @Direct
loadXmlResourceParser(int resId, String type)235     XmlResourceParser loadXmlResourceParser(int resId, String type);
236 
237     @Direct
loadXmlResourceParser(String file, int id, int assetCookie, String type)238     XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type);
239 
240     @Direct
loadDrawable(TypedValue value, int id)241     Drawable loadDrawable(TypedValue value, int id);
242 
243     @Direct
loadDrawable(TypedValue value, int id, Resources.Theme theme)244     Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme);
245 
246     @Direct
obtainAttributes(AttributeSet set, int[] attrs)247     TypedArray obtainAttributes(AttributeSet set, int[] attrs);
248 
249     @Direct
getQuantityString(int id, int quantity, Object... formatArgs)250     String getQuantityString(int id, int quantity, Object... formatArgs);
251 
252     @Direct
getQuantityString(int resId, int quantity)253     String getQuantityString(int resId, int quantity);
254 
255     @Direct
openRawResource(int id)256     InputStream openRawResource(int id);
257 
258     @Direct
openRawResourceFd(int id)259     AssetFileDescriptor openRawResourceFd(int id);
260 
261     @Direct
obtainTypedArray(int id)262     TypedArray obtainTypedArray(int id);
263 
264     @Direct
getAttributeSetSourceResId(AttributeSet attrs)265     int getAttributeSetSourceResId(AttributeSet attrs);
266 
267     @Direct
updateConfiguration( Configuration config, DisplayMetrics metrics, CompatibilityInfo compat)268     void updateConfiguration(
269         Configuration config, DisplayMetrics metrics, CompatibilityInfo compat);
270   }
271 }
272