package com.android.ide.common.resources; import com.android.ide.common.rendering.api.DensityBasedResourceValue; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.StyleResourceValue; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.resources.Density; import com.android.resources.ResourceType; import com.google.common.collect.Lists; import junit.framework.TestCase; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; public class ResourceResolverTest extends TestCase { public void test() throws Exception { TestResourceRepository frameworkRepository = TestResourceRepository.create(true, new Object[]{ "values/strings.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <string name=\"ok\">Ok</string>\n" + "</resources>\n", "values/themes.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <style name=\"Theme\">\n" + " <item name=\"colorForeground\">@android:color/bright_foreground_dark</item>\n" + " <item name=\"colorBackground\">@android:color/background_dark</item>\n" + " </style>\n" + " <style name=\"Theme.Light\">\n" + " <item name=\"colorBackground\">@android:color/background_light</item>\n" + " <item name=\"colorForeground\">@color/bright_foreground_light</item>\n" + " </style>\n" + "</resources>\n", "values/colors.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <color name=\"background_dark\">#ff000000</color>\n" + " <color name=\"background_light\">#ffffffff</color>\n" + " <color name=\"bright_foreground_dark\">@android:color/background_light</color>\n" + " <color name=\"bright_foreground_light\">@android:color/background_dark</color>\n" + "</resources>\n", "values/ids.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <item name=\"some_framework_id\" type=\"id\" />\n" + "</resources>\n", }); TestResourceRepository projectRepository = TestResourceRepository.create(false, new Object[]{ "layout/layout1.xml", "<!--contents doesn't matter-->", "layout/layout2.xml", "<!--contents doesn't matter-->", "layout-land/layout1.xml", "<!--contents doesn't matter-->", "layout-land/onlyLand.xml", "<!--contents doesn't matter-->", "drawable/graphic.9.png", new byte[0], "mipmap-xhdpi/ic_launcher.png", new byte[0], "values/styles.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <style name=\"MyTheme\" parent=\"android:Theme.Light\">\n" + " <item name=\"android:textColor\">#999999</item>\n" + " <item name=\"foo\">?android:colorForeground</item>\n" + " </style>\n" + " <style name=\"MyTheme.Dotted1\" parent=\"\">\n" + " </style>" + " <style name=\"MyTheme.Dotted2\">\n" + " </style>" + " <style name=\"RandomStyle\">\n" + " <item name=\"android:text\">© Copyright</item>\n" + " </style>" + " <style name=\"RandomStyle2\" parent=\"RandomStyle\">\n" + " </style>" + " <style name=\"Theme.FakeTheme\" parent=\"\">\n" + " </style>" + " <style name=\"Theme\" parent=\"RandomStyle\">\n" + " </style>" + "</resources>\n", "values/strings.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <item type=\"id\" name=\"action_bar_refresh\" />\n" + " <item type=\"dimen\" name=\"dialog_min_width_major\">45%</item>\n" + " <string name=\"home_title\">Home Sample</string>\n" + " <string name=\"show_all_apps\">All</string>\n" + " <string name=\"menu_wallpaper\">Wallpaper</string>\n" + " <string name=\"menu_search\">Search</string>\n" + " <string name=\"menu_settings\">Settings</string>\n" + " <string name=\"dummy\" translatable=\"false\">Ignore Me</string>\n" + " <string name=\"wallpaper_instructions\">Tap picture to set portrait wallpaper</string>\n" + "</resources>\n", "values-es/strings.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <string name=\"show_all_apps\">Todo</string>\n" + "</resources>\n", }); assertFalse(projectRepository.isFrameworkRepository()); FolderConfiguration config = FolderConfiguration.getConfigForFolder("values-es-land"); assertNotNull(config); Map<ResourceType, Map<String, ResourceValue>> projectResources = projectRepository.getConfiguredResources(config); Map<ResourceType, Map<String, ResourceValue>> frameworkResources = frameworkRepository.getConfiguredResources(config); assertNotNull(projectResources); ResourceResolver resolver = ResourceResolver.create(projectResources, frameworkResources, "MyTheme", true); assertNotNull(resolver); LayoutLog logger = new LayoutLog() { @Override public void warning(String tag, String message, Object data) { fail(message); } @Override public void fidelityWarning(String tag, String message, Throwable throwable, Object data) { fail(message); } @Override public void error(String tag, String message, Object data) { fail(message); } @Override public void error(String tag, String message, Throwable throwable, Object data) { fail(message); } }; resolver.setLogger(logger); assertEquals("MyTheme", resolver.getThemeName()); assertTrue(resolver.isProjectTheme()); // findResValue assertNotNull(resolver.findResValue("@string/show_all_apps", false)); assertNotNull(resolver.findResValue("@android:string/ok", false)); assertNotNull(resolver.findResValue("@android:string/ok", true)); assertEquals("Todo", resolver.findResValue("@string/show_all_apps", false).getValue()); assertEquals("Home Sample", resolver.findResValue("@string/home_title", false).getValue()); assertEquals("45%", resolver.findResValue("@dimen/dialog_min_width_major", false).getValue()); assertNotNull(resolver.findResValue("@android:color/bright_foreground_dark", true)); assertEquals("@android:color/background_light", resolver.findResValue("@android:color/bright_foreground_dark", true).getValue()); assertEquals("#ffffffff", resolver.findResValue("@android:color/background_light", true).getValue()); assertNull(resolver.findResValue("?attr/non_existent_style", false)); // shouldn't log an error. assertEquals(Density.XHIGH, ((DensityBasedResourceValue) resolver.findResValue("@mipmap/ic_launcher", false)) .getResourceDensity()); // also ensures that returned value is instance of DensityBasedResourceValue // getTheme StyleResourceValue myTheme = resolver.getTheme("MyTheme", false); assertNotNull(myTheme); assertSame(resolver.findResValue("@style/MyTheme", false), myTheme); assertNull(resolver.getTheme("MyTheme", true)); assertNull(resolver.getTheme("MyNonexistentTheme", true)); StyleResourceValue themeLight = resolver.getTheme("Theme.Light", true); assertNotNull(themeLight); StyleResourceValue theme = resolver.getTheme("Theme", true); assertNotNull(theme); // getParent StyleResourceValue parent = resolver.getParent(myTheme); assertNotNull(parent); assertEquals("Theme.Light", parent.getName()); // themeIsParentOf assertTrue(resolver.themeIsParentOf(themeLight, myTheme)); assertFalse(resolver.themeIsParentOf(myTheme, themeLight)); assertTrue(resolver.themeIsParentOf(theme, themeLight)); assertFalse(resolver.themeIsParentOf(themeLight, theme)); assertTrue(resolver.themeIsParentOf(theme, myTheme)); assertFalse(resolver.themeIsParentOf(myTheme, theme)); StyleResourceValue dotted1 = resolver.getTheme("MyTheme.Dotted1", false); assertNotNull(dotted1); StyleResourceValue dotted2 = resolver.getTheme("MyTheme.Dotted2", false); assertNotNull(dotted2); assertTrue(resolver.themeIsParentOf(myTheme, dotted2)); assertFalse(resolver.themeIsParentOf(myTheme, dotted1)); // because parent="" // isTheme assertFalse(resolver.isTheme(resolver.findResValue("@style/RandomStyle", false), null)); assertFalse(resolver.isTheme(resolver.findResValue("@style/RandomStyle2", false), null)); assertFalse(resolver.isTheme(resolver.findResValue("@style/Theme.FakeTheme", false), null)); assertFalse(resolver.isTheme(resolver.findResValue("@style/Theme", false), null)); // check XML escaping in value resources StyleResourceValue randomStyle = (StyleResourceValue) resolver.findResValue( "@style/RandomStyle", false); assertEquals("\u00a9 Copyright", randomStyle.getItem("text", true).getValue()); assertTrue(resolver.isTheme(resolver.findResValue("@style/MyTheme.Dotted2", false), null)); assertFalse(resolver.isTheme(resolver.findResValue("@style/MyTheme.Dotted1", false), null)); assertTrue(resolver.isTheme(resolver.findResValue("@style/MyTheme", false), null)); assertTrue(resolver.isTheme(resolver.findResValue("@android:style/Theme.Light", false), null)); assertTrue(resolver.isTheme(resolver.findResValue("@android:style/Theme", false), null)); // findItemInStyle assertNotNull(resolver.findItemInStyle(myTheme, "colorForeground", true)); assertEquals("@color/bright_foreground_light", resolver.findItemInStyle(myTheme, "colorForeground", true).getValue()); assertNotNull(resolver.findItemInStyle(dotted2, "colorForeground", true)); assertNull(resolver.findItemInStyle(dotted1, "colorForeground", true)); // findItemInTheme assertNotNull(resolver.findItemInTheme("colorForeground", true)); assertEquals("@color/bright_foreground_light", resolver.findItemInTheme("colorForeground", true).getValue()); assertEquals("@color/bright_foreground_light", resolver.findResValue("?colorForeground", true).getValue()); ResourceValue target = new ResourceValue(ResourceType.STRING, "dummy", false); target.setValue("?foo"); assertEquals("#ff000000", resolver.resolveResValue(target).getValue()); // getFrameworkResource assertNull(resolver.getFrameworkResource(ResourceType.STRING, "show_all_apps")); assertNotNull(resolver.getFrameworkResource(ResourceType.STRING, "ok")); assertEquals("Ok", resolver.getFrameworkResource(ResourceType.STRING, "ok").getValue()); // getProjectResource assertNull(resolver.getProjectResource(ResourceType.STRING, "ok")); assertNotNull(resolver.getProjectResource(ResourceType.STRING, "show_all_apps")); assertEquals("Todo", resolver.getProjectResource(ResourceType.STRING, "show_all_apps").getValue()); // resolveResValue // android:color/bright_foreground_dark => @android:color/background_light => white assertEquals("Todo", resolver.resolveResValue( resolver.findResValue("@string/show_all_apps", false)).getValue()); assertEquals("#ffffffff", resolver.resolveResValue( resolver.findResValue("@android:color/bright_foreground_dark", false)).getValue()); // resolveValue assertEquals("#ffffffff", resolver.resolveValue(ResourceType.STRING, "bright_foreground_dark", "@android:color/background_light", true).getValue()); assertFalse(resolver.resolveValue(null, "id", "@+id/some_framework_id", false) .isFramework()); // error expected. boolean failed = false; ResourceValue val = null; try { val = resolver.resolveValue(ResourceType.STRING, "bright_foreground_dark", "@color/background_light", false); } catch (AssertionError expected) { failed = true; } assertTrue("incorrect resource returned: " + val, failed); // themeExtends assertTrue(resolver.themeExtends("@android:style/Theme", "@android:style/Theme")); assertTrue(resolver.themeExtends("@android:style/Theme", "@android:style/Theme.Light")); assertFalse(resolver.themeExtends("@android:style/Theme.Light", "@android:style/Theme")); assertTrue(resolver.themeExtends("@style/MyTheme.Dotted2", "@style/MyTheme.Dotted2")); assertTrue(resolver.themeExtends("@style/MyTheme", "@style/MyTheme.Dotted2")); assertTrue(resolver.themeExtends("@android:style/Theme.Light", "@style/MyTheme.Dotted2")); assertTrue(resolver.themeExtends("@android:style/Theme", "@style/MyTheme.Dotted2")); assertFalse(resolver.themeExtends("@style/MyTheme.Dotted1", "@style/MyTheme.Dotted2")); // Switch to MyTheme.Dotted1 (to make sure the parent="" inheritance works properly.) // To do that we need to create a new resource resolver. resolver = ResourceResolver.create(projectResources, frameworkResources, "MyTheme.Dotted1", true); resolver.setLogger(logger); assertNotNull(resolver); assertEquals("MyTheme.Dotted1", resolver.getThemeName()); assertTrue(resolver.isProjectTheme()); assertNull(resolver.findItemInTheme("colorForeground", true)); resolver = ResourceResolver.create(projectResources, frameworkResources, "MyTheme.Dotted2", true); resolver.setLogger(logger); assertNotNull(resolver); assertEquals("MyTheme.Dotted2", resolver.getThemeName()); assertTrue(resolver.isProjectTheme()); assertNotNull(resolver.findItemInTheme("colorForeground", true)); // Test recording resolver List<ResourceValue> chain = Lists.newArrayList(); resolver = ResourceResolver.create(projectResources, frameworkResources, "MyTheme", true); resolver = resolver.createRecorder(chain); assertNotNull(resolver.findResValue("@android:color/bright_foreground_dark", true)); ResourceValue v = resolver.findResValue("@android:color/bright_foreground_dark", false); chain.clear(); assertEquals("#ffffffff", resolver.resolveResValue(v).getValue()); assertEquals("@android:color/bright_foreground_dark => " + "@android:color/background_light => #ffffffff", ResourceItemResolver.getDisplayString("@android:color/bright_foreground_dark", chain)); frameworkRepository.dispose(); projectRepository.dispose(); } public void testMissingMessage() throws Exception { TestResourceRepository projectRepository = TestResourceRepository.create(false, new Object[]{ "values/colors.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <color name=\"loop1\">@color/loop1</color>\n" + " <color name=\"loop2a\">@color/loop2b</color>\n" + " <color name=\"loop2b\">@color/loop2a</color>\n" + "</resources>\n", }); assertFalse(projectRepository.isFrameworkRepository()); FolderConfiguration config = FolderConfiguration.getConfigForFolder("values-es-land"); assertNotNull(config); Map<ResourceType, Map<String, ResourceValue>> projectResources = projectRepository.getConfiguredResources(config); assertNotNull(projectResources); ResourceResolver resolver = ResourceResolver.create(projectResources, projectResources, "MyTheme", true); final AtomicBoolean wasWarned = new AtomicBoolean(false); LayoutLog logger = new LayoutLog() { @Override public void warning(String tag, String message, Object data) { if ("Couldn't resolve resource @android:string/show_all_apps".equals(message)) { wasWarned.set(true); } else { fail(message); } } }; resolver.setLogger(logger); assertNull(resolver.findResValue("@string/show_all_apps", true)); assertTrue(wasWarned.get()); projectRepository.dispose(); } public void testLoop() throws Exception { TestResourceRepository projectRepository = TestResourceRepository.create(false, new Object[]{ "values/colors.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <color name=\"loop1\">@color/loop1</color>\n" + " <color name=\"loop2a\">@color/loop2b</color>\n" + " <color name=\"loop2b\">@color/loop2a</color>\n" + "</resources>\n", }); assertFalse(projectRepository.isFrameworkRepository()); FolderConfiguration config = FolderConfiguration.getConfigForFolder("values-es-land"); assertNotNull(config); Map<ResourceType, Map<String, ResourceValue>> projectResources = projectRepository.getConfiguredResources(config); assertNotNull(projectResources); ResourceResolver resolver = ResourceResolver.create(projectResources, projectResources, "MyTheme", true); assertNotNull(resolver); final AtomicBoolean wasWarned = new AtomicBoolean(false); LayoutLog logger = new LayoutLog() { @Override public void error(String tag, String message, Object data) { if (("Potential stack overflow trying to resolve " + "'@color/loop1': cyclic resource definitions?" + " Render may not be accurate.").equals(message)) { wasWarned.set(true); } else if (("Potential stack overflow trying to resolve " + "'@color/loop2b': cyclic resource definitions? " + "Render may not be accurate.").equals(message)) { wasWarned.set(true); } else { fail(message); } } }; resolver.setLogger(logger); assertNotNull(resolver.findResValue("@color/loop1", false)); resolver.resolveResValue(resolver.findResValue("@color/loop1", false)); assertTrue(wasWarned.get()); wasWarned.set(false); assertNotNull(resolver.findResValue("@color/loop2a", false)); resolver.resolveResValue(resolver.findResValue("@color/loop2a", false)); assertTrue(wasWarned.get()); projectRepository.dispose(); } public void testParentCycle() throws IOException { TestResourceRepository projectRepository = TestResourceRepository.create(false, new Object[]{ "values/styles.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <style name=\"ButtonStyle.Base\">\n" + " <item name=\"android:textColor\">#ff0000</item>\n" + " </style>\n" + " <style name=\"ButtonStyle\" parent=\"ButtonStyle.Base\">\n" + " <item name=\"android:layout_height\">40dp</item>\n" + " </style>\n" + "</resources>\n", "layouts/layout.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + " android:layout_width=\"match_parent\"\n" + " android:layout_height=\"match_parent\">\n" + "\n" + " <TextView\n" + " style=\"@style/ButtonStyle\"\n" + " android:layout_width=\"wrap_content\"\n" + " android:layout_height=\"wrap_content\" />\n" + "\n" + "</RelativeLayout>\n", }); assertFalse(projectRepository.isFrameworkRepository()); FolderConfiguration config = FolderConfiguration.getConfigForFolder("values-es-land"); assertNotNull(config); Map<ResourceType, Map<String, ResourceValue>> projectResources = projectRepository.getConfiguredResources(config); assertNotNull(projectResources); ResourceResolver resolver = ResourceResolver.create(projectResources, projectResources, "ButtonStyle", true); assertNotNull(resolver); final AtomicBoolean wasWarned = new AtomicBoolean(false); LayoutLog logger = new LayoutLog() { @Override public void error(String tag, String message, Object data) { assertEquals("Cyclic style parent definitions: \"ButtonStyle\" specifies " + "parent \"ButtonStyle.Base\" implies parent \"ButtonStyle\"", message); assertEquals(LayoutLog.TAG_BROKEN, tag); wasWarned.set(true); } }; resolver.setLogger(logger); StyleResourceValue buttonStyle = (StyleResourceValue) resolver.findResValue( "@style/ButtonStyle", false); ResourceValue textColor = resolver.findItemInStyle(buttonStyle, "textColor", true); assertNotNull(textColor); assertEquals("#ff0000", textColor.getValue()); assertFalse(wasWarned.get()); ResourceValue missing = resolver.findItemInStyle(buttonStyle, "missing", true); assertNull(missing); assertTrue(wasWarned.get()); projectRepository.dispose(); } public void testSetDeviceDefaults() throws Exception { TestResourceRepository frameworkRepository = TestResourceRepository.create(true, new Object[] { "values/themes.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <style name=\"Theme.Light\" parent=\"\">\n" + " <item name=\"android:textColor\">#ff0000</item>\n" + " </style>\n" + " <style name=\"Theme.Holo.Light\" parent=\"Theme.Light\">\n" + " <item name=\"android:textColor\">#00ff00</item>\n" + " </style>\n" + " <style name=\"Theme.DeviceDefault.Light\" parent=\"Theme.Holo.Light\"/>\n" + " <style name=\"Theme\" parent=\"\">\n" + " <item name=\"android:textColor\">#000000</item>\n" + " </style>\n" + " <style name=\"Theme.Holo\" parent=\"Theme\">\n" + " <item name=\"android:textColor\">#0000ff</item>\n" + " </style>\n" + " <style name=\"Theme.DeviceDefault\" parent=\"Theme.Holo\"/>\n" + "</resources>\n", }); TestResourceRepository projectRepository = TestResourceRepository.create(false, new Object[] { "values/themes.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <style name=\"AppTheme\" parent=\"android:Theme.DeviceDefault.Light\"/>\n" + " <style name=\"AppTheme.Dark\" parent=\"android:Theme.DeviceDefault\"/>\n" + "</resources>\n" }); assertFalse(projectRepository.isFrameworkRepository()); FolderConfiguration config = FolderConfiguration.getConfigForFolder("values-es-land"); assertNotNull(config); Map<ResourceType, Map<String, ResourceValue>> projectResources = projectRepository .getConfiguredResources(config); Map<ResourceType, Map<String, ResourceValue>> frameworkResources = frameworkRepository .getConfiguredResources(config); assertNotNull(projectResources); ResourceResolver lightResolver = ResourceResolver.create(projectResources, frameworkResources, "AppTheme", true); assertNotNull(lightResolver); ResourceValue textColor = lightResolver.findItemInTheme("textColor", true); assertNotNull(textColor); assertEquals("#00ff00", textColor.getValue()); lightResolver.setDeviceDefaults("Theme.Light", null); textColor = lightResolver.findItemInTheme("textColor", true); assertNotNull(textColor); assertEquals("#ff0000", textColor.getValue()); ResourceResolver darkResolver = ResourceResolver.create(projectResources, frameworkResources, "AppTheme.Dark", true); assertNotNull(darkResolver); textColor = darkResolver.findItemInTheme("textColor", true); assertNotNull(textColor); assertEquals("#0000ff", textColor.getValue()); darkResolver.setDeviceDefaults("Theme.Light", "Theme"); textColor = darkResolver.findItemInTheme("textColor", true); assertNotNull(textColor); assertEquals("#000000", textColor.getValue()); } public void testCycle() throws Exception { TestResourceRepository frameworkRepository = TestResourceRepository.create(true, new Object[] { "values/themes.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <style name=\"Theme.DeviceDefault.Light\"/>\n" + "</resources>\n", }); TestResourceRepository projectRepository = TestResourceRepository.create(false, new Object[] { "values/themes.xml", "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources>\n" + " <style name=\"AppTheme\" parent=\"android:Theme.DeviceDefault.Light\"/>\n" + " <style name=\"AppTheme.Dark\" parent=\"android:Theme.DeviceDefault\"/>\n" + " <style name=\"foo\" parent=\"bar\"/>\n" + " <style name=\"bar\" parent=\"foo\"/>\n" + "</resources>\n" }); assertFalse(projectRepository.isFrameworkRepository()); FolderConfiguration config = FolderConfiguration.getConfigForFolder("values"); assertNotNull(config); Map<ResourceType, Map<String, ResourceValue>> projectResources = projectRepository .getConfiguredResources(config); Map<ResourceType, Map<String, ResourceValue>> frameworkResources = frameworkRepository .getConfiguredResources(config); assertNotNull(projectResources); ResourceResolver resolver = ResourceResolver.create(projectResources, frameworkResources, "AppTheme", true); final AtomicBoolean wasWarned = new AtomicBoolean(false); LayoutLog logger = new LayoutLog() { @Override public void error(String tag, String message, Object data) { if ("Cyclic style parent definitions: \"foo\" specifies parent \"bar\" specifies parent \"foo\"".equals(message)) { wasWarned.set(true); } else { fail(message); } } }; resolver.setLogger(logger); assertFalse(resolver.isTheme(resolver.findResValue("@style/foo", false), null)); assertTrue(wasWarned.get()); projectRepository.dispose(); } }