Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import static android.os.Build.VERSION_CODES.N;
      4 import static android.os.Build.VERSION_CODES.O;
      5 import static org.robolectric.Shadows.shadowOf;
      6 import static org.robolectric.shadow.api.Shadow.directlyOn;
      7 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
      8 
      9 import android.content.res.AssetFileDescriptor;
     10 import android.content.res.Resources;
     11 import android.content.res.ResourcesImpl;
     12 import android.content.res.TypedArray;
     13 import android.content.res.XmlResourceParser;
     14 import android.graphics.drawable.Drawable;
     15 import android.os.ParcelFileDescriptor;
     16 import android.util.AttributeSet;
     17 import android.util.LongSparseArray;
     18 import android.util.TypedValue;
     19 import java.io.FileInputStream;
     20 import java.io.IOException;
     21 import java.io.InputStream;
     22 import java.lang.reflect.Field;
     23 import java.lang.reflect.Modifier;
     24 import java.util.ArrayList;
     25 import java.util.List;
     26 import java.util.Locale;
     27 import org.robolectric.annotation.HiddenApi;
     28 import org.robolectric.annotation.Implementation;
     29 import org.robolectric.annotation.Implements;
     30 import org.robolectric.annotation.RealObject;
     31 import org.robolectric.annotation.Resetter;
     32 import org.robolectric.res.Plural;
     33 import org.robolectric.res.PluralRules;
     34 import org.robolectric.res.ResName;
     35 import org.robolectric.res.ResType;
     36 import org.robolectric.res.ResourceTable;
     37 import org.robolectric.res.TypedResource;
     38 import org.robolectric.util.ReflectionHelpers;
     39 
     40 @Implements(value = ResourcesImpl.class, isInAndroidSdk = false, minSdk = N)
     41 public class ShadowResourcesImpl {
     42   private static List<LongSparseArray<?>> resettableArrays;
     43 
     44   @RealObject
     45   ResourcesImpl realResourcesImpl;
     46 
     47   @Resetter
     48   public static void reset() {
     49     if (resettableArrays == null) {
     50       resettableArrays = obtainResettableArrays();
     51     }
     52     for (LongSparseArray<?> sparseArray : resettableArrays) {
     53       sparseArray.clear();
     54     }
     55   }
     56 
     57   private static List<LongSparseArray<?>> obtainResettableArrays() {
     58     List<LongSparseArray<?>> resettableArrays = new ArrayList<>();
     59     Field[] allFields = Resources.class.getDeclaredFields();
     60     for (Field field : allFields) {
     61       if (Modifier.isStatic(field.getModifiers()) && field.getType().equals(LongSparseArray.class)) {
     62         field.setAccessible(true);
     63         try {
     64           LongSparseArray<?> longSparseArray = (LongSparseArray<?>) field.get(null);
     65           if (longSparseArray != null) {
     66             resettableArrays.add(longSparseArray);
     67           }
     68         } catch (IllegalAccessException e) {
     69           throw new RuntimeException(e);
     70         }
     71       }
     72     }
     73     return resettableArrays;
     74   }
     75 
     76   @Implementation
     77   public String getQuantityString(int id, int quantity, Object... formatArgs) throws Resources.NotFoundException {
     78     String raw = getQuantityString(id, quantity);
     79     return String.format(Locale.ENGLISH, raw, formatArgs);
     80   }
     81 
     82   @Implementation
     83   public String getQuantityString(int resId, int quantity) throws Resources.NotFoundException {
     84     ShadowAssetManager shadowAssetManager = shadowOf(realResourcesImpl.getAssets());
     85 
     86     TypedResource typedResource = shadowAssetManager.getResourceTable().getValue(resId, shadowAssetManager.config);
     87     if (typedResource != null && typedResource instanceof PluralRules) {
     88       PluralRules pluralRules = (PluralRules) typedResource;
     89       Plural plural = pluralRules.find(quantity);
     90 
     91       if (plural == null) {
     92         return null;
     93       }
     94 
     95       TypedResource<?> resolvedTypedResource = shadowAssetManager.resolve(
     96           new TypedResource<>(plural.getString(), ResType.CHAR_SEQUENCE, pluralRules.getXmlContext()), shadowAssetManager.config, resId);
     97       return resolvedTypedResource == null ? null : resolvedTypedResource.asString();
     98     } else {
     99       return null;
    100     }
    101   }
    102 
    103   @Implementation
    104   public InputStream openRawResource(int id) throws Resources.NotFoundException {
    105     ShadowAssetManager shadowAssetManager = shadowOf(realResourcesImpl.getAssets());
    106     ResourceTable resourceTable = shadowAssetManager.getResourceTable();
    107     InputStream inputStream = resourceTable.getRawValue(id, shadowAssetManager.config);
    108     if (inputStream == null) {
    109       throw newNotFoundException(id);
    110     } else {
    111       return inputStream;
    112     }
    113   }
    114 
    115   /**
    116    * Since {@link AssetFileDescriptor}s are not yet supported by Robolectric, {@code null} will
    117    * be returned if the resource is found. If the resource cannot be found, {@link Resources.NotFoundException} will
    118    * be thrown.
    119    */
    120   @Implementation
    121   public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
    122     InputStream inputStream = openRawResource(id);
    123     if (!(inputStream instanceof FileInputStream)) {
    124       // todo fixme
    125       return null;
    126     }
    127 
    128     FileInputStream fis = (FileInputStream) inputStream;
    129     try {
    130       return new AssetFileDescriptor(ParcelFileDescriptor.dup(fis.getFD()), 0, fis.getChannel().size());
    131     } catch (IOException e) {
    132       throw newNotFoundException(id);
    133     }
    134   }
    135 
    136   private Resources.NotFoundException newNotFoundException(int id) {
    137     ResourceTable resourceTable = shadowOf(realResourcesImpl.getAssets()).getResourceTable();
    138     ResName resName = resourceTable.getResName(id);
    139     if (resName == null) {
    140       return new Resources.NotFoundException("resource ID #0x" + Integer.toHexString(id));
    141     } else {
    142       return new Resources.NotFoundException(resName.getFullyQualifiedName());
    143     }
    144   }
    145 
    146   @HiddenApi
    147   @Implementation
    148   public XmlResourceParser loadXmlResourceParser(int resId, String type) throws Resources.NotFoundException {
    149     ShadowAssetManager shadowAssetManager = shadowOf(realResourcesImpl.getAssets());
    150     return shadowAssetManager.loadXmlResourceParser(resId, type);
    151   }
    152 
    153   @HiddenApi @Implementation
    154   public XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws Resources.NotFoundException {
    155     return loadXmlResourceParser(id, type);
    156   }
    157 
    158   @Implements(value = ResourcesImpl.ThemeImpl.class, minSdk = N, isInAndroidSdk = false)
    159   public static class ShadowThemeImpl {
    160     @RealObject ResourcesImpl.ThemeImpl realThemeImpl;
    161 
    162     @Implementation
    163     public TypedArray obtainStyledAttributes(Resources.Theme wrapper, AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
    164       Resources resources = wrapper.getResources();
    165       return shadowOf(resources.getAssets()).attrsToTypedArray(resources, set, attrs, defStyleAttr, getNativePtr(), defStyleRes);
    166     }
    167 
    168     public long getNativePtr() {
    169       return ReflectionHelpers.getField(realThemeImpl, "mTheme");
    170     }
    171   }
    172 
    173   @Implementation
    174   public Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme, boolean useCache) throws Resources.NotFoundException {
    175     Drawable drawable = directlyOn(realResourcesImpl, ResourcesImpl.class, "loadDrawable",
    176         from(Resources.class, wrapper),
    177         from(TypedValue.class, value),
    178         from(int.class, id),
    179         from(Resources.Theme.class, theme),
    180         from(boolean.class, useCache)
    181     );
    182 
    183     ShadowResources.setCreatedFromResId(wrapper, id, drawable);
    184     return drawable;
    185   }
    186 
    187   @Implementation(minSdk = O)
    188   public Drawable loadDrawable(Resources wrapper,  TypedValue value, int id, int density, Resources.Theme theme) {
    189     Drawable drawable = directlyOn(realResourcesImpl, ResourcesImpl.class, "loadDrawable",
    190         from(Resources.class, wrapper),
    191         from(TypedValue.class, value),
    192         from(int.class, id),
    193         from(int.class, density),
    194         from(Resources.Theme.class, theme));
    195 
    196     ShadowResources.setCreatedFromResId(wrapper, id, drawable);
    197     return drawable;
    198   }
    199 }
    200