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