blob: 907e493d9bdeda71c440f7d7c17485626e9f87c8 [file] [log] [blame]
package org.robolectric.shadows;
import android.content.res.Resources;
import android.graphics.Color;
import android.util.TypedValue;
import org.robolectric.res.AttrData;
import org.robolectric.res.Attribute;
import org.robolectric.res.DrawableNode;
import org.robolectric.res.DrawableResourceLoader;
import org.robolectric.res.FsFile;
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.util.Util;
import java.util.LinkedHashMap;
import java.util.Map;
public class Converter<T> {
private static int nextStringCookie = 0xbaaa5;
private static final Map<String, ResType> ATTR_TYPE_MAP = new LinkedHashMap<String, ResType>();
static {
ATTR_TYPE_MAP.put("boolean", ResType.BOOLEAN);
ATTR_TYPE_MAP.put("color", ResType.COLOR);
ATTR_TYPE_MAP.put("dimension", ResType.DIMEN);
ATTR_TYPE_MAP.put("float", ResType.FLOAT);
ATTR_TYPE_MAP.put("integer", ResType.INTEGER);
ATTR_TYPE_MAP.put("string", ResType.CHAR_SEQUENCE);
}
synchronized private static int getNextStringCookie() {
return nextStringCookie++;
}
public static void convertAndFill(Attribute attribute, TypedValue outValue, ResourceLoader resourceLoader, String qualifiers) {
if (attribute == null || attribute.isNull()) {
outValue.type = TypedValue.TYPE_NULL;
return;
}
TypedResource attrTypeData = resourceLoader.getValue(attribute.resName, qualifiers);
if (attrTypeData == null) {
throw new Resources.NotFoundException("Couldn't find " + attribute.resName + " attr data");
}
AttrData attrData = (AttrData) attrTypeData.getData();
convertAndFill(attribute, outValue, resourceLoader, qualifiers, attrData);
}
public static void convertAndFill(Attribute attribute, TypedValue outValue, ResourceLoader resourceLoader, String qualifiers, AttrData attrData) {
// short-circuit Android caching of loaded resources cuz our string positions don't remain stable...
outValue.assetCookie = getNextStringCookie();
String format = attrData.getFormat();
String[] types = format.split("\\|");
// dereference resource and style references...
if (attribute.isStyleReference()) {
ResName resName = attribute.getStyleReference();
// todo
System.out.println("TODO: Not handling " + attribute.value + " yet!");
return;
}
ResourceIndex resourceIndex = resourceLoader.getResourceIndex();
while (attribute.isResourceReference()) {
ResName resName = attribute.getResourceReference();
Integer resourceId = resourceIndex.getResourceId(resName);
if (resourceId == null) {
throw new Resources.NotFoundException("unknown resource " + resName);
}
outValue.type = TypedValue.TYPE_REFERENCE;
outValue.resourceId = resourceId;
TypedResource dereferencedRef = resourceLoader.getValue(resName, qualifiers);
if (dereferencedRef == null) {
if (resName.type.equals("id")) {
return;
} else if (resName.type.equals("layout")) {
return; // resourceId is good enough, right?
} else if (resName.type.equals("dimen")) {
return;
} else if (DrawableResourceLoader.isStillHandledHere(resName)) {
// wtf. color and drawable references reference are all kinds of stupid.
DrawableNode drawableNode = resourceLoader.getDrawableNode(resName, qualifiers);
if (drawableNode == null) {
throw new Resources.NotFoundException("can't find file for " + resName);
} else {
outValue.type = TypedValue.TYPE_STRING;
outValue.data = 0;
outValue.assetCookie = getNextStringCookie();
outValue.string = drawableNode.getFsFile().getPath();
return;
}
} else {
throw new RuntimeException("huh? " + resName);
}
} else {
if (dereferencedRef.isFile()) {
outValue.type = TypedValue.TYPE_STRING;
outValue.data = 0;
outValue.assetCookie = getNextStringCookie();
outValue.string = dereferencedRef.asString();
return;
} else if (dereferencedRef.getData() instanceof String) {
attribute = new Attribute(attribute.resName, dereferencedRef.asString(), resName.packageName);
if (attribute.isResourceReference()) {
continue;
}
}
}
break;
}
if (attribute.isNull()) {
outValue.type = TypedValue.TYPE_NULL;
return;
}
// Special case for attrs that can be integers or enums, like numColumns.
// todo: generalize this!
if (format.equals("integer|enum") || format.equals("dimension|enum")) {
if (attribute.value.matches("^\\d.*")) {
types = new String[] { types[0] };
} else {
types = new String[] { "enum" };
}
}
for (String type : types) {
if ("reference".equals(type)) continue; // already handled above
Converter converter = ATTR_TYPE_MAP.containsKey(type)
? getConverter(ATTR_TYPE_MAP.get(type))
: null;
if (converter == null) {
if (type.equals("enum")) {
converter = new EnumConverter(attrData);
} else if (type.equals("flag")) {
converter = new FlagConverter(attrData);
}
}
if (converter != null) {
try {
converter.fillTypedValue(attribute.value, outValue);
} catch (Exception e) {
throw new RuntimeException("error converting " + attribute.value + " using " + converter.getClass().getSimpleName(), e);
}
return;
}
}
}
public static Converter getConverter(ResType resType) {
switch (resType) {
case ATTR_DATA:
return new FromAttrData();
case BOOLEAN:
return new FromBoolean();
case CHAR_SEQUENCE:
return new FromCharSequence();
case COLOR:
return new FromColor();
case COLOR_STATE_LIST:
return new FromFilePath();
case DIMEN:
return new FromDimen();
case FILE:
return new FromFile();
case FLOAT:
return new FromFloat();
case INTEGER:
return new FromInt();
case LAYOUT:
return new FromFilePath();
case CHAR_SEQUENCE_ARRAY:
case INTEGER_ARRAY:
return new FromArray();
default:
throw new UnsupportedOperationException(resType.name());
}
}
public CharSequence asCharSequence(TypedResource typedResource) {
throw cantDo("asCharSequence");
}
public int asInt(TypedResource typedResource) {
throw cantDo("asInt");
}
public TypedResource[] getItems(TypedResource typedResource) {
throw cantDo("getItems");
}
public void fillTypedValue(T data, TypedValue typedValue) {
throw cantDo("fillTypedValue");
}
private UnsupportedOperationException cantDo(String operation) {
return new UnsupportedOperationException(getClass().getName() + " doesn't support " + operation);
}
public static class FromAttrData extends Converter<AttrData> {
@Override public CharSequence asCharSequence(TypedResource typedResource) {
return typedResource.asString();
}
@Override public void fillTypedValue(AttrData data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_STRING;
throw new RuntimeException("huh?");
}
}
public static class FromCharSequence extends Converter<String> {
@Override public CharSequence asCharSequence(TypedResource typedResource) {
return typedResource.asString();
}
@Override public int asInt(TypedResource typedResource) {
String rawValue = typedResource.asString();
return convertInt(rawValue);
}
@Override public void fillTypedValue(String data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_STRING;
typedValue.data = 0;
typedValue.assetCookie = getNextStringCookie();
typedValue.string = data;
}
}
public static class FromColor extends Converter<String> {
@Override public void fillTypedValue(String data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_INT_COLOR_ARGB8;
typedValue.data = Color.parseColor(data);
typedValue.assetCookie = 0;
}
}
private static class FromFilePath extends Converter<String> {
@Override public void fillTypedValue(String data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_STRING;
typedValue.data = 0;
typedValue.string = data;
typedValue.assetCookie = getNextStringCookie();
}
}
public static class FromArray extends Converter {
@Override public TypedResource[] getItems(TypedResource typedResource) {
return (TypedResource[]) typedResource.getData();
}
}
private static class FromInt extends Converter<String> {
@Override public void fillTypedValue(String data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_INT_HEX;
typedValue.data = convertInt(data);
typedValue.assetCookie = 0;
}
@Override public int asInt(TypedResource typedResource) {
String rawValue = typedResource.asString();
return convertInt(rawValue);
}
}
private static class FromFile extends Converter<FsFile> {
@Override public void fillTypedValue(FsFile data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_STRING;
typedValue.data = 0;
typedValue.string = data.getPath();
typedValue.assetCookie = getNextStringCookie();
}
}
private static class FromFloat extends Converter<String> {
@Override public void fillTypedValue(String data, TypedValue typedValue) {
ResourceHelper.parseFloatAttribute(null, data, typedValue, false);
}
}
private static class FromBoolean extends Converter<String> {
@Override public void fillTypedValue(String data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_INT_BOOLEAN;
typedValue.data = convertBool(data) ? 1 : 0;
typedValue.assetCookie = 0;
}
}
private static class FromDimen extends Converter<String> {
@Override public void fillTypedValue(String data, TypedValue typedValue) {
ResourceHelper.parseFloatAttribute(null, data, typedValue, false);
}
}
///////////////////////
private static int convertInt(String rawValue) {
try {
// Decode into long, because there are some large hex values in the android resource files
// (e.g. config_notificationsBatteryLowARGB = 0xFFFF0000 in sdk 14).
// Integer.decode() does not support large, i.e. negative values in hex numbers.
// try parsing decimal number
return (int) Long.parseLong(rawValue);
} catch (NumberFormatException nfe) {
// try parsing hex number
try {
return Long.decode(rawValue).intValue();
} catch (NumberFormatException e) {
throw new RuntimeException(rawValue + " is not an integer.", nfe);
}
}
}
private static boolean convertBool(String rawValue) {
if ("true".equalsIgnoreCase(rawValue)) {
return true;
} else if ("false".equalsIgnoreCase(rawValue)) {
return false;
}
try {
int intValue = Integer.parseInt(rawValue);
return intValue != 0;
} catch (NumberFormatException e) {
throw new RuntimeException(e);
}
}
private static class EnumConverter extends EnumOrFlagConverter {
public EnumConverter(AttrData attrData) {
super(attrData);
}
@Override public void fillTypedValue(String data, TypedValue typedValue) {
typedValue.type = TypedValue.TYPE_INT_HEX;
typedValue.data = findValueFor(data);
typedValue.assetCookie = 0;
}
}
private static class FlagConverter extends EnumOrFlagConverter {
public FlagConverter(AttrData attrData) {
super(attrData);
}
@Override public void fillTypedValue(String data, TypedValue typedValue) {
int flags = 0;
for (String key : data.split("\\|")) {
flags |= findValueFor(key);
}
typedValue.type = TypedValue.TYPE_INT_HEX;
typedValue.data = flags;
typedValue.assetCookie = 0;
}
}
private static class EnumOrFlagConverter extends Converter<String> {
private final AttrData attrData;
public EnumOrFlagConverter(AttrData attrData) {
this.attrData = attrData;
}
protected int findValueFor(String key) {
String valueFor = attrData.getValueFor(key);
if (valueFor == null) {
throw new RuntimeException("no value found for " + key);
}
return Util.parseInt(valueFor);
}
}
}