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