ShadowAssetManager dereferences attrs better
ShadowAssetManager now looks in the full style tree (children and
themes) while dereferencing parent styles.
In order, ShadowAssetManager will search:
- All known children in this style path
- The Theme Style
- The ResourceLoader
Improves the logging information when a parent style can't be found.
diff --git a/src/main/java/org/robolectric/res/AttrData.java b/src/main/java/org/robolectric/res/AttrData.java
index 3bec1d0..0ebc561 100644
--- a/src/main/java/org/robolectric/res/AttrData.java
+++ b/src/main/java/org/robolectric/res/AttrData.java
@@ -45,10 +45,23 @@
}
@Override public String toString() {
- return "AttrData{" +
- "name='" + name + '\'' +
- ", format='" + format + '\'' +
- '}';
+
+ StringBuilder builder = new StringBuilder("AttrData{name='")
+ .append(name)
+ .append("', format='")
+ .append(format)
+ .append('\'');
+ if (pairs != null) {
+ for (Pair p : pairs) {
+ builder.append(' ')
+ .append(p.name)
+ .append("='")
+ .append(p.value)
+ .append('\'');
+ }
+ }
+ builder.append('}');
+ return builder.toString();
}
public static class Pair {
diff --git a/src/main/java/org/robolectric/res/ResName.java b/src/main/java/org/robolectric/res/ResName.java
index c532b07..7196df7 100644
--- a/src/main/java/org/robolectric/res/ResName.java
+++ b/src/main/java/org/robolectric/res/ResName.java
@@ -127,7 +127,7 @@
public void mustBe(String expectedType) {
if (!type.equals(expectedType)) {
- throw new RuntimeException("expected " + getFullyQualifiedName() + " to be a " + expectedType);
+ throw new RuntimeException("expected " + getFullyQualifiedName() + " to be a " + expectedType + ", is a " + type);
}
}
}
diff --git a/src/main/java/org/robolectric/shadows/ShadowAssetManager.java b/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
index 32d9a73..58ac810 100644
--- a/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
+++ b/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
@@ -13,6 +13,8 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
import org.jetbrains.annotations.NotNull;
import org.robolectric.AndroidManifest;
import org.robolectric.Robolectric;
@@ -123,7 +125,7 @@
ResName themeStyleName = resourceIndex.getResName(shadowOf(theTheme).getStyleResourceId());
if (themeStyleName == null) return false; // is this right?
- Style themeStyle = resolveStyle(resourceLoader, themeStyleName, getQualifiers());
+ Style themeStyle = resolveStyle(resourceLoader, null, themeStyleName, getQualifiers());
//// Load the theme attribute for the default style attributes. E.g., attr/buttonStyle
//ResName defStyleName = resourceLoader.getResourceIndex().getResName(ident);
@@ -258,7 +260,7 @@
ResourceIndex resourceIndex = resourceLoader.getResourceIndex();
ResName resName = resourceIndex.getResName(styleRes);
- Style style = resolveStyle(resourceLoader, resName, assertManager.getQualifiers());
+ Style style = resolveStyle(resourceLoader, null, resName, assertManager.getQualifiers());
appliedThemeStyles.get(theme).add(new OverlayedStyle(style, force));
}
@@ -292,11 +294,11 @@
return themesById.get(internalThemeId);
}
- static Style resolveStyle(ResourceLoader resourceLoader, @NotNull ResName themeStyleName, String qualifiers) {
+ static Style resolveStyle(ResourceLoader resourceLoader, Style appTheme, @NotNull ResName themeStyleName, String qualifiers) {
TypedResource themeStyleResource = resourceLoader.getValue(themeStyleName, qualifiers);
if (themeStyleResource == null) return null;
StyleData themeStyleData = (StyleData) themeStyleResource.getData();
- return new StyleResolver(resourceLoader, themeStyleData, themeStyleName, qualifiers);
+ return new StyleResolver(resourceLoader, themeStyleData, appTheme, themeStyleName, qualifiers);
}
TypedResource getAndResolve(int resId, String qualifiers, boolean resolveRefs) {
@@ -362,41 +364,147 @@
static class StyleResolver implements Style {
private final ResourceLoader resourceLoader;
- private final StyleData leafStyle;
+ private final List<StyleData> styles = new ArrayList<StyleData>();
+ private final Style theme;
private final ResName myResName;
private final String qualifiers;
- public StyleResolver(ResourceLoader resourceLoader, StyleData styleData, ResName myResName, String qualifiers) {
+ public StyleResolver(ResourceLoader resourceLoader, StyleData styleData,
+ Style theme, ResName myResName, String qualifiers) {
this.resourceLoader = resourceLoader;
- this.leafStyle = styleData;
+ this.theme = theme;
this.myResName = myResName;
this.qualifiers = qualifiers;
+ styles.add(styleData);
}
@Override public Attribute getAttrValue(ResName resName) {
resName.mustBe("attr");
- StyleData currentStyle = leafStyle;
- while (currentStyle != null) {
- Attribute value = currentStyle.getAttrValue(resName);
+ for (StyleData style : styles) {
+ Attribute value = style.getAttrValue(resName);
if (value != null) return value;
- currentStyle = getParent(currentStyle);
}
+ int initialSize = styles.size();
+ while (hasParent(styles.get(styles.size() - 1))) {
+ StyleData parent = getParent(styles.get(styles.size() - 1));
+ if (parent != null) {
+ styles.add(parent);
+ } else {
+ break;
+ }
+ }
+ for (int i = initialSize; i < styles.size(); i++) {
+ StyleData style = styles.get(i);
+ Attribute value = style.getAttrValue(resName);
+ if (value != null) return value;
+ }
+ if (theme != null) {
+ Attribute value = theme.getAttrValue(resName);
+ if (value != null) return value;
+ }
+
return null;
}
- private StyleData getParent(StyleData currentStyle) {
- String parent = currentStyle.getParent();
+ private static String getParentStyleName(StyleData style) {
+ if (style == null) {
+ return null;
+ }
+ String parent = style.getParent();
+ if (parent == null || parent.isEmpty()) {
+ parent = null;
+ String name = style.getName();
+ if (name.contains(".")) {
+ parent = name.substring(0, name.lastIndexOf('.'));
+ if (parent.isEmpty()) {
+ return null;
+ }
+ }
+ }
+ return parent;
+ }
- if (parent == null || parent.isEmpty()) return null;
+ private static boolean hasParent(StyleData style) {
+ if (style == null) return false;
+ String parent = style.getParent();
+ return parent != null && !parent.isEmpty();
+ }
+
+ private StyleData getParent(StyleData style) {
+ String parent = getParentStyleName(style);
+
+ if (parent == null) return null;
if (parent.startsWith("@")) parent = parent.substring(1);
- ResName style = ResName.qualifyResName(parent, currentStyle.getPackageName(), "style");
- TypedResource typedResource = resourceLoader.getValue(style, qualifiers);
+ ResName styleRef = ResName.qualifyResName(parent, style.getPackageName(), "style");
+
+ styleRef = dereferenceResName(styleRef);
+
+ TypedResource typedResource = resourceLoader.getValue(styleRef, qualifiers);
+
if (typedResource == null) {
- throw new RuntimeException("huh? can't find parent for " + currentStyle);
+ StringBuilder builder = new StringBuilder("Could not find any resource ")
+ .append(" from reference ").append(styleRef)
+ .append(" from style ").append(style)
+ .append(" with theme ").append(theme);
+ throw new RuntimeException(builder.toString());
}
- return (StyleData) typedResource.getData();
+
+ Object data = typedResource.getData();
+ if (data instanceof StyleData) {
+ return (StyleData) data;
+ } else {
+ StringBuilder builder = new StringBuilder(styleRef.toString())
+ .append(" does not resolve to a Style.")
+ .append(" got ").append(data).append(" instead. ")
+ .append(" from style ").append(style)
+ .append(" with theme ").append(theme);
+ throw new RuntimeException(builder.toString());
+ }
}
+
+ private ResName dereferenceResName(ResName res) {
+ ResName styleRef = res;
+ boolean dereferencing = true;
+ while ("attr".equals(styleRef.type) && dereferencing) {
+ dereferencing = false;
+ for (StyleData parentStyle : styles) {
+ Attribute value = parentStyle.getAttrValue(styleRef);
+ if (value != null) {
+ styleRef = dereferenceAttr(value);
+ dereferencing = true;
+ break;
+ }
+ }
+ if (!dereferencing && theme != null) {
+ Attribute value = theme.getAttrValue(styleRef);
+ if (value != null) {
+ styleRef = dereferenceAttr(value);
+ dereferencing = true;
+ }
+ }
+ }
+
+ return styleRef;
+ }
+
+ private ResName dereferenceAttr(Attribute attr) {
+ if (attr.isResourceReference()) {
+ return attr.getResourceReference();
+ } else if (attr.isStyleReference()) {
+ return attr.getStyleReference();
+ }
+ throw new RuntimeException("Found a " + attr + " but can't cast it :(");
+ }
+
+ public String toString() {
+ return "StyleResolver{"
+ + "name=" + myResName
+ + ", of=" + styles.get(0)
+ + "}";
+ }
+
}
+
}
diff --git a/src/main/java/org/robolectric/shadows/ShadowResources.java b/src/main/java/org/robolectric/shadows/ShadowResources.java
index f698026..348c4f2 100644
--- a/src/main/java/org/robolectric/shadows/ShadowResources.java
+++ b/src/main/java/org/robolectric/shadows/ShadowResources.java
@@ -123,7 +123,7 @@
ResName themeStyleName = getResName(themeResourceId);
if (DEBUG) System.out.println("themeStyleName = " + themeStyleName);
- theme = ShadowAssetManager.resolveStyle(resourceLoader, themeStyleName, shadowAssetManager.getQualifiers());
+ theme = ShadowAssetManager.resolveStyle(resourceLoader, null, themeStyleName, shadowAssetManager.getQualifiers());
if (defStyleAttr != 0) {
// Load the theme attribute for the default style attributes. E.g., attr/buttonStyle
@@ -145,7 +145,7 @@
if (defStyleAttribute.isResourceReference()) {
ResName defStyleResName = defStyleAttribute.getResourceReference();
- defStyleFromAttr = ShadowAssetManager.resolveStyle(resourceLoader, defStyleResName, shadowAssetManager.getQualifiers());
+ defStyleFromAttr = ShadowAssetManager.resolveStyle(resourceLoader, theme, defStyleResName, shadowAssetManager.getQualifiers());
}
}
}
@@ -162,7 +162,7 @@
styleAttributeResName = attrValue.getStyleReference();
}
}
- styleAttrStyle = ShadowAssetManager.resolveStyle(resourceLoader, styleAttributeResName, shadowAssetManager.getQualifiers());
+ styleAttrStyle = ShadowAssetManager.resolveStyle(resourceLoader, theme, styleAttributeResName, shadowAssetManager.getQualifiers());
}
if (defStyleRes != 0) {
@@ -177,7 +177,7 @@
}
}
}
- defStyleFromRes = ShadowAssetManager.resolveStyle(resourceLoader, resName, shadowAssetManager.getQualifiers());
+ defStyleFromRes = ShadowAssetManager.resolveStyle(resourceLoader, theme, resName, shadowAssetManager.getQualifiers());
}
List<Attribute> attributes = new ArrayList<Attribute>();
diff --git a/src/test/java/org/robolectric/R.java b/src/test/java/org/robolectric/R.java
index 32db3cc..5397554 100644
--- a/src/test/java/org/robolectric/R.java
+++ b/src/test/java/org/robolectric/R.java
@@ -330,6 +330,7 @@
public static final int Theme_ThirdTheme = 0x11008;
public static final int MyBlackTheme = 0x11009;
public static final int MyBlueTheme = 0x1100a;
+ public static final int IndirectButtonStyle = 0x1100b;
}
public static final class fraction {
diff --git a/src/test/java/org/robolectric/shadows/ThemeTest.java b/src/test/java/org/robolectric/shadows/ThemeTest.java
index 47a872d..2228167 100644
--- a/src/test/java/org/robolectric/shadows/ThemeTest.java
+++ b/src/test/java/org/robolectric/shadows/ThemeTest.java
@@ -90,6 +90,7 @@
TestActivity activity = buildActivity(TestActivityWithAnotherTheme.class).create().get();
ResourceLoader resourceLoader = Robolectric.shadowOf(activity.getResources()).getResourceLoader();
Style style = ShadowAssetManager.resolveStyle(resourceLoader,
+ null,
new ResName(TestUtil.TEST_PACKAGE, "style", "Widget.AnotherTheme.Button.Blarf"), "");
assertThat(style.getAttrValue(new ResName("android", "attr", "background")).value)
.isEqualTo("#ffff0000");
@@ -99,10 +100,23 @@
TestActivity activity = buildActivity(TestActivityWithAnotherTheme.class).create().get();
ResourceLoader resourceLoader = Robolectric.shadowOf(activity.getResources()).getResourceLoader();
Style style = ShadowAssetManager.resolveStyle(resourceLoader,
+ null,
new ResName(TestUtil.TEST_PACKAGE, "style", "Theme.MyTheme"), "");
assertThat(style.getAttrValue(new ResName("android", "attr", "background"))).isNull();
}
+
+ @Test public void shouldApplyParentStylesFromAttrs() throws Exception {
+ TestActivity activity = buildActivity(TestActivityWithAnotherTheme.class).create().get();
+ ResourceLoader resourceLoader = Robolectric.shadowOf(activity.getResources()).getResourceLoader();
+ Style theme = ShadowAssetManager.resolveStyle(resourceLoader, null,
+ new ResName(TestUtil.TEST_PACKAGE, "style", "Theme.AnotherTheme"), "");
+ Style style = ShadowAssetManager.resolveStyle(resourceLoader, theme,
+ new ResName(TestUtil.TEST_PACKAGE, "style", "IndirectButtonStyle"), "");
+ assertThat(style.getAttrValue(new ResName("android", "attr", "background")).value)
+ .isEqualTo("#ffff0000");
+ }
+
public static class TestActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/src/test/resources/res/values/themes.xml b/src/test/resources/res/values/themes.xml
index 88e9b4d..e30f4af 100644
--- a/src/test/resources/res/values/themes.xml
+++ b/src/test/resources/res/values/themes.xml
@@ -54,4 +54,9 @@
<item name="android:windowBackground">@color/blue</item>
<item name="android:textColor">@color/white</item>
</style>
+
+ <style name="IndirectButtonStyle" parent="@android:attr/buttonStyle">
+ <item name="android:minHeight">12dp</item>
+ </style>
+
</resources>