Home | History | Annotate | Download | only in shadows
      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
     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
     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
     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
     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
    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
    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
    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 
    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
    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
    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
    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)
    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)
    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 
    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 
    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 
    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)
    290     protected TypedArray obtainStyledAttributes(int[] attrs) {
    291       return obtainStyledAttributes(0, attrs);
    292     }
    293 
    294     @Implementation(maxSdk = M)
    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)
    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 
    306     private ShadowLegacyAssetManager getShadowAssetManager() {
    307       return legacyShadowOf(getResources().getAssets());
    308     }
    309 
    310     private Resources getResources() {
    311       return ReflectionHelpers.getField(realTheme, "this$0");
    312     }
    313   }
    314 
    315 
    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 
    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
    350     protected void __constructor__() {}
    351 
    352     @Implementation
    353     protected void __constructor__(String name) {
    354       this.message = name;
    355     }
    356 
    357     @Override @Implementation
    358     public String toString() {
    359       return realObject.getClass().getName() + ": " + message;
    360     }
    361   }
    362 }
    363