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