blob: 3e67a9e16f16d71e9b20d46678af676566c61617 [file] [log] [blame]
package org.robolectric.shadows;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
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 android.view.Display;
import org.jetbrains.annotations.NotNull;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.res.Attribute;
import org.robolectric.res.Plural;
import org.robolectric.res.ResName;
import org.robolectric.res.ResType;
import org.robolectric.res.ResourceIndex;
import org.robolectric.res.ResourceLoader;
import org.robolectric.res.TypedResource;
import org.robolectric.res.builder.ResourceParser;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.res.builder.XmlBlock;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static org.robolectric.internal.Shadow.directlyOn;
import static org.robolectric.Shadows.shadowOf;
/**
* Shadow for {@link android.content.res.Resources}.
*/
@Implements(Resources.class)
public class ShadowResources {
private static Resources system = null;
private static List<LongSparseArray<?>> resettableArrays;
private float density = 1.0f;
private DisplayMetrics displayMetrics;
private Display display;
@RealObject Resources realResources;
@Resetter
public static void reset() {
if (resettableArrays == null) {
resettableArrays = obtainResettableArrays();
}
for (LongSparseArray<?> sparseArray : resettableArrays) {
sparseArray.clear();
}
}
private static List<LongSparseArray<?>> obtainResettableArrays() {
List<LongSparseArray<?>> 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;
}
@Implementation
public static Resources getSystem() {
if (system == null) {
AssetManager assetManager = AssetManager.getSystem();
DisplayMetrics metrics = new DisplayMetrics();
Configuration config = new Configuration();
system = new Resources(assetManager, metrics, config);
}
return system;
}
private TypedArray attrsToTypedArray(AttributeSet set, int[] attrs, int defStyleAttr, int themeResourceId, int defStyleRes) {
if (set == null) {
set = new RoboAttributeSet(new ArrayList<Attribute>(), shadowOf(realResources.getAssets()).getResourceLoader());
}
List<Attribute> attributes = shadowOf(realResources.getAssets()).buildAttributes(set, attrs, defStyleAttr, themeResourceId, defStyleRes);
TypedArray typedArray = createTypedArray(attributes, attrs);
shadowOf(typedArray).positionDescription = set.getPositionDescription();
return typedArray;
}
public TypedArray createTypedArray(List<Attribute> set, int[] attrs) {
ResourceLoader resourceLoader = getResourceLoader();
ResourceIndex resourceIndex = resourceLoader.getResourceIndex();
String qualifiers = shadowOf(realResources.getAssets()).getQualifiers();
CharSequence[] stringData = new CharSequence[attrs.length];
int[] data = new int[attrs.length * ShadowAssetManager.STYLE_NUM_ENTRIES];
int[] indices = new int[attrs.length + 1];
int nextIndex = 0;
for (int i = 0; i < attrs.length; i++) {
int offset = i * ShadowAssetManager.STYLE_NUM_ENTRIES;
int attr = attrs[i];
ResName attrName = resourceIndex.getResName(attr);
if (attrName != null) {
Attribute attribute = Attribute.find(set, attrName);
TypedValue typedValue = new TypedValue();
Converter.convertAndFill(attribute, typedValue, resourceLoader, qualifiers, true);
if (attribute != null && !attribute.isNull()) {
//noinspection PointlessArithmeticExpression
data[offset + ShadowAssetManager.STYLE_TYPE] = typedValue.type;
data[offset + ShadowAssetManager.STYLE_DATA] = typedValue.type == TypedValue.TYPE_STRING ? i : typedValue.data;
data[offset + ShadowAssetManager.STYLE_ASSET_COOKIE] = typedValue.assetCookie;
data[offset + ShadowAssetManager.STYLE_RESOURCE_ID] = typedValue.resourceId;
data[offset + ShadowAssetManager.STYLE_CHANGING_CONFIGURATIONS] = typedValue.changingConfigurations;
data[offset + ShadowAssetManager.STYLE_DENSITY] = typedValue.density;
stringData[i] = typedValue.string;
indices[nextIndex + 1] = i;
nextIndex++;
}
}
}
indices[0] = nextIndex;
return ShadowTypedArray.create(realResources, attrs, data, indices, nextIndex, stringData);
}
@Implementation
public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
return attrsToTypedArray(set, attrs, 0, 0, 0);
}
@Implementation
public String getResourceName(int resId) throws Resources.NotFoundException {
return getResName(resId).getFullyQualifiedName();
}
@Implementation
public String getResourcePackageName(int resId) throws Resources.NotFoundException {
return getResName(resId).packageName;
}
@Implementation
public String getResourceTypeName(int resId) throws Resources.NotFoundException {
return getResName(resId).type;
}
@Implementation
public String getResourceEntryName(int resId) throws Resources.NotFoundException {
return getResName(resId).name;
}
private @NotNull ResName getResName(int id) {
return shadowOf(realResources.getAssets()).getResName(id);
}
private String getQualifiers() {
return shadowOf(realResources.getAssets()).getQualifiers();
}
@Implementation
public CharSequence getText(int id) throws Resources.NotFoundException {
CharSequence text = directlyOn(realResources, Resources.class).getText(id);
return text.toString();
}
@Implementation
public String getQuantityString(int id, int quantity, Object... formatArgs) throws Resources.NotFoundException {
String raw = getQuantityString(id, quantity);
return String.format(Locale.ENGLISH, raw, formatArgs);
}
@Implementation
public String getQuantityString(int id, int quantity) throws Resources.NotFoundException {
ResName resName = getResName(id);
Plural plural = getResourceLoader().getPlural(resName, quantity, getQualifiers());
String string = plural.getString();
ShadowAssetManager shadowAssetManager = shadowOf(realResources.getAssets());
TypedResource<?> typedResource = shadowAssetManager.resolve(
new TypedResource<>(string, ResType.CHAR_SEQUENCE), getQualifiers(),
new ResName(resName.packageName, "string", resName.name));
return typedResource == null ? null : typedResource.asString();
}
@Implementation
public InputStream openRawResource(int id) throws Resources.NotFoundException {
return getResourceLoader().getRawValue(getResName(id));
}
@Implementation
public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
try {
FileInputStream fis = (FileInputStream)openRawResource(id);
return new AssetFileDescriptor(ParcelFileDescriptor.dup(fis.getFD()), 0, fis.getChannel().size());
} catch (Exception e) {
return null;
}
}
public void setDensity(float density) {
this.density = density;
if (displayMetrics != null) {
displayMetrics.density = density;
}
}
public void setScaledDensity(float scaledDensity) {
if (displayMetrics != null) {
displayMetrics.scaledDensity = scaledDensity;
}
}
public void setDisplay(Display display) {
this.display = display;
displayMetrics = null;
}
@Implementation
public DisplayMetrics getDisplayMetrics() {
if (displayMetrics == null) {
if (display == null) {
display = ReflectionHelpers.callConstructor(Display.class);
}
displayMetrics = new DisplayMetrics();
display.getMetrics(displayMetrics);
}
displayMetrics.density = this.density;
return displayMetrics;
}
@HiddenApi @Implementation
public XmlResourceParser loadXmlResourceParser(int id, String type) throws Resources.NotFoundException {
ResName resName = shadowOf(realResources.getAssets()).resolveResName(id);
XmlBlock block = getResourceLoader().getXml(resName, getQualifiers());
if (block == null) {
throw new Resources.NotFoundException();
}
return ResourceParser.from(block, resName.packageName, getResourceLoader().getResourceIndex());
}
@HiddenApi @Implementation
public XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws Resources.NotFoundException {
return loadXmlResourceParser(id, type);
}
public ResourceLoader getResourceLoader() {
return shadowOf(realResources.getAssets()).getResourceLoader();
}
@Implements(Resources.Theme.class)
public static class ShadowTheme {
@RealObject Resources.Theme realTheme;
protected Resources resources;
private int styleResourceId;
@Implementation
public void applyStyle(int resid, boolean force) {
if (styleResourceId == 0) {
this.styleResourceId = resid;
}
ShadowAssetManager.applyThemeStyle(styleResourceId, resid, force);
}
@Implementation
public void setTo(Resources.Theme other) {
this.styleResourceId = shadowOf(other).styleResourceId;
}
public int getStyleResourceId() {
return styleResourceId;
}
@Implementation
public TypedArray obtainStyledAttributes(int[] attrs) {
return obtainStyledAttributes(0, attrs);
}
@Implementation
public TypedArray obtainStyledAttributes(int resid, int[] attrs) throws android.content.res.Resources.NotFoundException {
return obtainStyledAttributes(null, attrs, 0, resid);
}
@Implementation
public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
return shadowOf(getResources()).attrsToTypedArray(set, attrs, defStyleAttr, styleResourceId, defStyleRes);
}
@Implementation
public Resources getResources() {
return ReflectionHelpers.getField(realTheme, "this$0");
}
}
@Implementation
public final Resources.Theme newTheme() {
Resources.Theme theme = directlyOn(realResources, Resources.class).newTheme();
int themeId = Integer.valueOf(ReflectionHelpers.getField(theme, "mTheme").toString()); // TODO: in Lollipop, these can be longs, which will overflow int
shadowOf(realResources.getAssets()).setTheme(themeId, theme);
return theme;
}
@HiddenApi @Implementation
public Drawable loadDrawable(TypedValue value, int id) {
ResName resName = shadowOf(realResources.getAssets()).tryResName(id);
Drawable drawable = directlyOn(realResources, Resources.class, "loadDrawable",
ClassParameter.from(TypedValue.class, value), ClassParameter.from(int.class, id));
// todo: this kinda sucks, find some better way...
if (drawable != null) {
shadowOf(drawable).createdFromResId = id;
if (drawable instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
ShadowBitmap shadowBitmap = shadowOf(bitmap);
if (shadowBitmap.createdFromResId == -1) {
shadowBitmap.setCreatedFromResId(id, resName);
}
}
}
}
return drawable;
}
@Implementation
public Drawable loadDrawable(TypedValue value, int id, Resources.Theme theme) throws Resources.NotFoundException {
ResName resName = shadowOf(realResources.getAssets()).tryResName(id);
Drawable drawable = directlyOn(realResources, Resources.class, "loadDrawable",
ClassParameter.from(TypedValue.class, value), ClassParameter.from(int.class, id), ClassParameter.from(Resources.Theme.class, theme));
// todo: this kinda sucks, find some better way...
if (drawable != null) {
shadowOf(drawable).createdFromResId = id;
if (drawable instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
ShadowBitmap shadowBitmap = shadowOf(bitmap);
if (shadowBitmap.createdFromResId == -1) {
shadowBitmap.setCreatedFromResId(id, resName);
}
}
}
}
return drawable;
}
@Implements(Resources.NotFoundException.class)
public static class ShadowNotFoundException {
@RealObject Resources.NotFoundException realObject;
private String message;
public void __constructor__() {
}
public void __constructor__(String name) {
this.message = name;
}
@Implementation
public String toString() {
return realObject.getClass().getName() + ": " + message;
}
}
}