org.robolectric.shadows.ShadowResources Maven / Gradle / Ivy
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
import static android.os.Build.VERSION_CODES.Q;
import static org.robolectric.shadows.ShadowAssetManager.legacyShadowOf;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.content.res.ResourcesImpl;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
import android.os.ParcelFileDescriptor;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.LongSparseArray;
import android.util.TypedValue;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.Bootstrap;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.internal.bytecode.ShadowedObject;
import org.robolectric.res.Plural;
import org.robolectric.res.PluralRules;
import org.robolectric.res.ResName;
import org.robolectric.res.ResType;
import org.robolectric.res.ResourceTable;
import org.robolectric.res.TypedResource;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowLegacyResourcesImpl.ShadowLegacyThemeImpl;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
/** Shadow of {@link Resources}. */
@Implements(Resources.class)
public class ShadowResources {
private static Resources system = null;
private static List> resettableArrays;
@RealObject Resources realResources;
private final Set configurationChangeListeners = new HashSet<>();
@Resetter
public static void reset() {
if (resettableArrays == null) {
resettableArrays = obtainResettableArrays();
}
for (LongSparseArray> sparseArray : resettableArrays) {
sparseArray.clear();
}
system = null;
ReflectionHelpers.setStaticField(Resources.class, "mSystem", null);
}
@Implementation
protected static Resources getSystem() {
if (system == null) {
AssetManager assetManager = AssetManager.getSystem();
DisplayMetrics metrics = new DisplayMetrics();
Configuration config = new Configuration();
system = new Resources(assetManager, metrics, config);
Bootstrap.updateConfiguration(system);
}
return system;
}
@Implementation
protected TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
if (isLegacyAssetManager()) {
return legacyShadowOf(realResources.getAssets())
.attrsToTypedArray(realResources, set, attrs, 0, 0, 0);
} else {
return reflector(ResourcesReflector.class, realResources).obtainAttributes(set, attrs);
}
}
@Implementation
protected String getQuantityString(int id, int quantity, Object... formatArgs)
throws Resources.NotFoundException {
if (isLegacyAssetManager()) {
String raw = getQuantityString(id, quantity);
return String.format(Locale.ENGLISH, raw, formatArgs);
} else {
return reflector(ResourcesReflector.class, realResources)
.getQuantityString(id, quantity, formatArgs);
}
}
@Implementation
protected String getQuantityString(int resId, int quantity) throws Resources.NotFoundException {
if (isLegacyAssetManager()) {
ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
TypedResource typedResource =
shadowAssetManager.getResourceTable().getValue(resId, shadowAssetManager.config);
if (typedResource != null && typedResource instanceof PluralRules) {
PluralRules pluralRules = (PluralRules) typedResource;
Plural plural = pluralRules.find(quantity);
if (plural == null) {
return null;
}
TypedResource> resolvedTypedResource =
shadowAssetManager.resolve(
new TypedResource<>(
plural.getString(), ResType.CHAR_SEQUENCE, pluralRules.getXmlContext()),
shadowAssetManager.config,
resId);
return resolvedTypedResource == null ? null : resolvedTypedResource.asString();
} else {
return null;
}
} else {
return reflector(ResourcesReflector.class, realResources).getQuantityString(resId, quantity);
}
}
@Implementation
protected InputStream openRawResource(int id) throws Resources.NotFoundException {
if (isLegacyAssetManager()) {
ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
ResourceTable resourceTable = shadowAssetManager.getResourceTable();
InputStream inputStream = resourceTable.getRawValue(id, shadowAssetManager.config);
if (inputStream == null) {
throw newNotFoundException(id);
} else {
return inputStream;
}
} else {
return reflector(ResourcesReflector.class, realResources).openRawResource(id);
}
}
/**
* Since {@link AssetFileDescriptor}s are not yet supported by Robolectric, {@code null} will be
* returned if the resource is found. If the resource cannot be found, {@link
* Resources.NotFoundException} will be thrown.
*/
@Implementation
protected AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
if (isLegacyAssetManager()) {
InputStream inputStream = openRawResource(id);
if (!(inputStream instanceof FileInputStream)) {
// todo fixme
return null;
}
FileInputStream fis = (FileInputStream) inputStream;
try {
return new AssetFileDescriptor(
ParcelFileDescriptor.dup(fis.getFD()), 0, fis.getChannel().size());
} catch (IOException e) {
throw newNotFoundException(id);
}
} else {
return reflector(ResourcesReflector.class, realResources).openRawResourceFd(id);
}
}
private Resources.NotFoundException newNotFoundException(int id) {
ResourceTable resourceTable = legacyShadowOf(realResources.getAssets()).getResourceTable();
ResName resName = resourceTable.getResName(id);
if (resName == null) {
return new Resources.NotFoundException("resource ID #0x" + Integer.toHexString(id));
} else {
return new Resources.NotFoundException(resName.getFullyQualifiedName());
}
}
@Implementation
protected TypedArray obtainTypedArray(int id) throws Resources.NotFoundException {
if (isLegacyAssetManager()) {
ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
TypedArray typedArray = shadowAssetManager.getTypedArrayResource(realResources, id);
if (typedArray != null) {
return typedArray;
} else {
throw newNotFoundException(id);
}
} else {
return reflector(ResourcesReflector.class, realResources).obtainTypedArray(id);
}
}
@HiddenApi
@Implementation
protected XmlResourceParser loadXmlResourceParser(int resId, String type)
throws Resources.NotFoundException {
if (isLegacyAssetManager()) {
ShadowLegacyAssetManager shadowAssetManager = legacyShadowOf(realResources.getAssets());
return setSourceResourceId(shadowAssetManager.loadXmlResourceParser(resId, type), resId);
} else {
ResourcesReflector relectedResources = reflector(ResourcesReflector.class, realResources);
return setSourceResourceId(relectedResources.loadXmlResourceParser(resId, type), resId);
}
}
@HiddenApi
@Implementation
protected XmlResourceParser loadXmlResourceParser(
String file, int id, int assetCookie, String type) throws Resources.NotFoundException {
if (isLegacyAssetManager()) {
return loadXmlResourceParser(id, type);
} else {
ResourcesReflector relectedResources = reflector(ResourcesReflector.class, realResources);
return setSourceResourceId(
relectedResources.loadXmlResourceParser(file, id, assetCookie, type), id);
}
}
private static XmlResourceParser setSourceResourceId(XmlResourceParser parser, int resourceId) {
Object shadow = parser instanceof ShadowedObject ? Shadow.extract(parser) : null;
if (shadow instanceof ShadowXmlBlock.ShadowParser) {
((ShadowXmlBlock.ShadowParser) shadow).setSourceResourceId(resourceId);
}
return parser;
}
@HiddenApi
@Implementation(maxSdk = KITKAT_WATCH)
protected Drawable loadDrawable(TypedValue value, int id) {
Drawable drawable = reflector(ResourcesReflector.class, realResources).loadDrawable(value, id);
setCreatedFromResId(realResources, id, drawable);
return drawable;
}
@Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
protected Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme)
throws Resources.NotFoundException {
Drawable drawable =
reflector(ResourcesReflector.class, realResources).loadDrawable(value, id, theme);
setCreatedFromResId(realResources, id, drawable);
return drawable;
}
private static List> obtainResettableArrays() {
List> resettableArrays = new ArrayList<>();
Field[] allFields = Resources.class.getDeclaredFields();
for (Field field : allFields) {
if (Modifier.isStatic(field.getModifiers())
&& field.getType().equals(LongSparseArray.class)) {
field.setAccessible(true);
try {
LongSparseArray> longSparseArray = (LongSparseArray>) field.get(null);
if (longSparseArray != null) {
resettableArrays.add(longSparseArray);
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return resettableArrays;
}
/**
* Returns the layout resource id the attribute set was inflated from. Backwards compatible
* version of {@link Resources#getAttributeSetSourceResId(AttributeSet)}, passes through to the
* underlying implementation on API levels where it is supported.
*/
@Implementation(minSdk = Q)
public static int getAttributeSetSourceResId(AttributeSet attrs) {
if (RuntimeEnvironment.getApiLevel() >= Q) {
return reflector(ResourcesReflector.class).getAttributeSetSourceResId(attrs);
} else {
Object shadow = attrs instanceof ShadowedObject ? Shadow.extract(attrs) : null;
return shadow instanceof ShadowXmlBlock.ShadowParser
? ((ShadowXmlBlock.ShadowParser) shadow).getSourceResourceId()
: 0;
}
}
/**
* Listener callback that's called when the configuration is updated for a resources. The callback
* receives the old and new configs (and can use {@link Configuration#diff(Configuration)} to
* produce a diff). The callback is called after the configuration has been applied to the
* underlying resources, so obtaining resources will use the new configuration in the callback.
*/
public interface OnConfigurationChangeListener {
void onConfigurationChange(
Configuration oldConfig, Configuration newConfig, DisplayMetrics newMetrics);
}
/**
* Add a listener to observe resource configuration changes. See {@link
* OnConfigurationChangeListener}.
*/
public void addConfigurationChangeListener(OnConfigurationChangeListener listener) {
configurationChangeListeners.add(listener);
}
/**
* Remove a listener to observe resource configuration changes. See {@link
* OnConfigurationChangeListener}.
*/
public void removeConfigurationChangeListener(OnConfigurationChangeListener listener) {
configurationChangeListeners.remove(listener);
}
@Implementation
protected void updateConfiguration(
Configuration config, DisplayMetrics metrics, CompatibilityInfo compat) {
Configuration oldConfig;
try {
oldConfig = new Configuration(realResources.getConfiguration());
} catch (NullPointerException e) {
// In old versions of Android the resource constructor calls updateConfiguration, in the
// app compat ResourcesWrapper subclass the reference to the underlying resources hasn't been
// configured yet, so it'll throw an NPE, catch this to avoid crashing.
oldConfig = null;
}
reflector(ResourcesReflector.class, realResources).updateConfiguration(config, metrics, compat);
if (oldConfig != null && config != null) {
for (OnConfigurationChangeListener listener : configurationChangeListeners) {
listener.onConfigurationChange(oldConfig, config, metrics);
}
}
}
/** Base class for shadows of {@link Resources.Theme}. */
public abstract static class ShadowTheme {
/** Shadow picker for {@link ShadowTheme}. */
public static class Picker extends ResourceModeShadowPicker {
public Picker() {
super(ShadowLegacyTheme.class, null, null);
}
}
}
/** Shadow for {@link Resources.Theme}. */
@Implements(value = Resources.Theme.class, shadowPicker = ShadowTheme.Picker.class)
public static class ShadowLegacyTheme extends ShadowTheme {
@RealObject Resources.Theme realTheme;
long getNativePtr() {
if (RuntimeEnvironment.getApiLevel() >= N) {
ResourcesImpl.ThemeImpl themeImpl = ReflectionHelpers.getField(realTheme, "mThemeImpl");
return ((ShadowLegacyThemeImpl) Shadow.extract(themeImpl)).getNativePtr();
} else {
return ((Number) ReflectionHelpers.getField(realTheme, "mTheme")).longValue();
}
}
@Implementation(maxSdk = M)
protected TypedArray obtainStyledAttributes(int[] attrs) {
return obtainStyledAttributes(0, attrs);
}
@Implementation(maxSdk = M)
protected TypedArray obtainStyledAttributes(int resid, int[] attrs)
throws Resources.NotFoundException {
return obtainStyledAttributes(null, attrs, 0, resid);
}
@Implementation(maxSdk = M)
protected TypedArray obtainStyledAttributes(
AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
return getShadowAssetManager()
.attrsToTypedArray(
innerGetResources(), set, attrs, defStyleAttr, getNativePtr(), defStyleRes);
}
private ShadowLegacyAssetManager getShadowAssetManager() {
return legacyShadowOf(innerGetResources().getAssets());
}
private Resources innerGetResources() {
if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) {
return realTheme.getResources();
}
return ReflectionHelpers.getField(realTheme, "this$0");
}
}
static void setCreatedFromResId(Resources resources, int id, Drawable drawable) {
// todo: this kinda sucks, find some better way...
if (drawable != null && Shadow.extract(drawable) instanceof ShadowDrawable) {
ShadowDrawable shadowDrawable = Shadow.extract(drawable);
String resourceName;
try {
resourceName = resources.getResourceName(id);
} catch (NotFoundException e) {
resourceName = "Unknown resource #0x" + Integer.toHexString(id);
}
shadowDrawable.setCreatedFromResId(id, resourceName);
}
}
private boolean isLegacyAssetManager() {
return ShadowAssetManager.useLegacy();
}
/** Shadow for {@link Resources.NotFoundException}. */
@Implements(Resources.NotFoundException.class)
public static class ShadowNotFoundException {
@RealObject Resources.NotFoundException realObject;
private String message;
@Implementation
protected void __constructor__() {}
@Implementation
protected void __constructor__(String name) {
this.message = name;
}
@Override
@Implementation
public String toString() {
return realObject.getClass().getName() + ": " + message;
}
}
@ForType(Resources.class)
interface ResourcesReflector {
@Direct
XmlResourceParser loadXmlResourceParser(int resId, String type);
@Direct
XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type);
@Direct
Drawable loadDrawable(TypedValue value, int id);
@Direct
Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme);
@Direct
TypedArray obtainAttributes(AttributeSet set, int[] attrs);
@Direct
String getQuantityString(int id, int quantity, Object... formatArgs);
@Direct
String getQuantityString(int resId, int quantity);
@Direct
InputStream openRawResource(int id);
@Direct
AssetFileDescriptor openRawResourceFd(int id);
@Direct
TypedArray obtainTypedArray(int id);
@Direct
int getAttributeSetSourceResId(AttributeSet attrs);
@Direct
void updateConfiguration(
Configuration config, DisplayMetrics metrics, CompatibilityInfo compat);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy