/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.layoutlib.bridge.intensive; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.Result; import com.android.ide.common.rendering.api.SessionParams; import com.android.ide.common.rendering.api.SessionParams.RenderingMode; import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.resources.FrameworkResources; import com.android.ide.common.resources.ResourceItem; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.io.FolderWrapper; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.RenderParamsFlags; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.layoutlib.bridge.impl.RenderAction; import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator; import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback; import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser; import com.android.resources.Density; import com.android.resources.Navigation; import com.android.resources.ResourceType; import com.android.utils.ILogger; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.util.DisplayMetrics; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.TimeUnit; import com.google.android.collect.Lists; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * This is a set of tests that loads all the framework resources and a project checked in this * test's resources. The main dependencies * are: * 1. Fonts directory. * 2. Framework Resources. * 3. App resources. * 4. build.prop file * * These are configured by two variables set in the system properties. * * 1. platform.dir: This is the directory for the current platform in the built SDK * (.../sdk/platforms/android-). * * The fonts are platform.dir/data/fonts. * The Framework resources are platform.dir/data/res. * build.prop is at platform.dir/build.prop. * * 2. test_res.dir: This is the directory for the resources of the test. If not specified, this * falls back to getClass().getProtectionDomain().getCodeSource().getLocation() * * The app resources are at: test_res.dir/testApp/MyApplication/app/src/main/res */ public class Main { private static final String PLATFORM_DIR_PROPERTY = "platform.dir"; private static final String RESOURCE_DIR_PROPERTY = "test_res.dir"; private static final String PLATFORM_DIR; private static final String TEST_RES_DIR; /** Location of the app to test inside {@link #TEST_RES_DIR}*/ private static final String APP_TEST_DIR = "/testApp/MyApplication"; /** Location of the app's res dir inside {@link #TEST_RES_DIR}*/ private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res"; private static LayoutLog sLayoutLibLog; private static FrameworkResources sFrameworkRepo; private static ResourceRepository sProjectResources; private static ILogger sLogger; private static Bridge sBridge; /** List of log messages generated by a render call. It can be used to find specific errors */ private static ArrayList sRenderMessages = Lists.newArrayList(); @Rule public static TestWatcher sRenderMessageWatcher = new TestWatcher() { @Override protected void succeeded(Description description) { // We only check error messages if the rest of the test case was successful. if (!sRenderMessages.isEmpty()) { fail(description.getMethodName() + " render error message: " + sRenderMessages.get (0)); } } }; static { // Test that System Properties are properly set. PLATFORM_DIR = getPlatformDir(); if (PLATFORM_DIR == null) { fail(String.format("System Property %1$s not properly set. The value is %2$s", PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY))); } TEST_RES_DIR = getTestResDir(); if (TEST_RES_DIR == null) { fail(String.format("System property %1$s.dir not properly set. The value is %2$s", RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY))); } } private static String getPlatformDir() { String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY); if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) { return platformDir; } // System Property not set. Try to find the directory in the build directory. String androidHostOut = System.getenv("ANDROID_HOST_OUT"); if (androidHostOut != null) { platformDir = getPlatformDirFromHostOut(new File(androidHostOut)); if (platformDir != null) { return platformDir; } } String workingDirString = System.getProperty("user.dir"); File workingDir = new File(workingDirString); // Test if workingDir is android checkout root. platformDir = getPlatformDirFromRoot(workingDir); if (platformDir != null) { return platformDir; } // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge. File currentDir = workingDir; if (currentDir.getName().equalsIgnoreCase("bridge")) { currentDir = currentDir.getParentFile(); } // Test if currentDir is platform/frameworks/base/tools/layoutlib. That is, root should be // workingDir/../../../../ (4 levels up) for (int i = 0; i < 4; i++) { if (currentDir != null) { currentDir = currentDir.getParentFile(); } } return currentDir == null ? null : getPlatformDirFromRoot(currentDir); } private static String getPlatformDirFromRoot(File root) { if (!root.isDirectory()) { return null; } File out = new File(root, "out"); if (!out.isDirectory()) { return null; } File host = new File(out, "host"); if (!host.isDirectory()) { return null; } File[] hosts = host.listFiles(path -> path.isDirectory() && (path.getName().startsWith("linux-") || path.getName().startsWith("darwin-"))); for (File hostOut : hosts) { String platformDir = getPlatformDirFromHostOut(hostOut); if (platformDir != null) { return platformDir; } } return null; } private static String getPlatformDirFromHostOut(File out) { if (!out.isDirectory()) { return null; } File sdkDir = new File(out, "sdk"); if (!sdkDir.isDirectory()) { return null; } File[] sdkDirs = sdkDir.listFiles(path -> { // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7) return path.isDirectory() && path.getName().startsWith("sdk"); }); for (File dir : sdkDirs) { String platformDir = getPlatformDirFromHostOutSdkSdk(dir); if (platformDir != null) { return platformDir; } } return null; } private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) { File[] possibleSdks = sdkDir.listFiles( path -> path.isDirectory() && path.getName().contains("android-sdk")); for (File possibleSdk : possibleSdks) { File platformsDir = new File(possibleSdk, "platforms"); File[] platforms = platformsDir.listFiles( path -> path.isDirectory() && path.getName().startsWith("android-")); if (platforms == null || platforms.length == 0) { continue; } Arrays.sort(platforms, (o1, o2) -> { final int MAX_VALUE = 1000; String suffix1 = o1.getName().substring("android-".length()); String suffix2 = o2.getName().substring("android-".length()); int suff1, suff2; try { suff1 = Integer.parseInt(suffix1); } catch (NumberFormatException e) { suff1 = MAX_VALUE; } try { suff2 = Integer.parseInt(suffix2); } catch (NumberFormatException e) { suff2 = MAX_VALUE; } if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) { return suff2 - suff1; } return suffix2.compareTo(suffix1); }); return platforms[0].getAbsolutePath(); } return null; } private static String getTestResDir() { String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY); if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) { return resourceDir; } // TEST_RES_DIR not explicitly set. Fallback to the class's source location. try { URL location = Main.class.getProtectionDomain().getCodeSource().getLocation(); return new File(location.getPath()).exists() ? location.getPath() : null; } catch (NullPointerException e) { // Prevent a lot of null checks by just catching the exception. return null; } } /** * Initialize the bridge and the resource maps. */ @BeforeClass public static void setUp() { File data_dir = new File(PLATFORM_DIR, "data"); File res = new File(data_dir, "res"); sFrameworkRepo = new FrameworkResources(new FolderWrapper(res)); sFrameworkRepo.loadResources(); sFrameworkRepo.loadPublicResources(getLogger()); sProjectResources = new ResourceRepository(new FolderWrapper(TEST_RES_DIR + APP_TEST_RES), false) { @NonNull @Override protected ResourceItem createResourceItem(@NonNull String name) { return new ResourceItem(name); } }; sProjectResources.loadResources(); File fontLocation = new File(data_dir, "fonts"); File buildProp = new File(PLATFORM_DIR, "build.prop"); File attrs = new File(res, "values" + File.separator + "attrs.xml"); sBridge = new Bridge(); sBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation, ConfigGenerator.getEnumMap(attrs), getLayoutLog()); } @Before public void beforeTestCase() { sRenderMessages.clear(); } /** Test activity.xml */ @Test public void testActivity() throws ClassNotFoundException { renderAndVerify("activity.xml", "activity.png"); } /** Test allwidgets.xml */ @Test public void testAllWidgets() throws ClassNotFoundException { renderAndVerify("allwidgets.xml", "allwidgets.png"); // We expect fidelity warnings for Path.isConvex. Fail for anything else. sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported.")); } @Test public void testArrayCheck() throws ClassNotFoundException { renderAndVerify("array_check.xml", "array_check.png"); } @Test public void testAllWidgetsTablet() throws ClassNotFoundException { renderAndVerify("allwidgets.xml", "allwidgets_tab.png", ConfigGenerator.NEXUS_7_2012); // We expect fidelity warnings for Path.isConvex. Fail for anything else. sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported.")); } private static void gc() { // See RuntimeUtil#gc in jlibs (http://jlibs.in/) Object obj = new Object(); WeakReference ref = new WeakReference(obj); obj = null; while(ref.get() != null) { System.gc(); } } @AfterClass public static void tearDown() { sLayoutLibLog = null; sFrameworkRepo = null; sProjectResources = null; sLogger = null; sBridge = null; gc(); System.out.println("Objects still linked from the DelegateManager:"); DelegateManager.dump(System.out); } /** Test expand_layout.xml */ @Test public void testExpand() throws ClassNotFoundException { // Create the layout pull parser. LayoutPullParser parser = createLayoutPullParser("expand_vert_layout.xml"); // Create LayoutLibCallback. LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); layoutLibCallback.initResources(); ConfigGenerator customConfigGenerator = new ConfigGenerator() .setScreenWidth(300) .setScreenHeight(20) .setDensity(Density.XHIGH) .setNavigation(Navigation.NONAV); SessionParams params = getSessionParams(parser, customConfigGenerator, layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false, RenderingMode.V_SCROLL, 22); renderAndVerify(params, "expand_vert_layout.png"); customConfigGenerator = new ConfigGenerator() .setScreenWidth(20) .setScreenHeight(300) .setDensity(Density.XHIGH) .setNavigation(Navigation.NONAV); parser = createLayoutPullParser("expand_horz_layout.xml"); params = getSessionParams(parser, customConfigGenerator, layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false, RenderingMode.H_SCROLL, 22); renderAndVerify(params, "expand_horz_layout.png"); } /** Test indeterminate_progressbar.xml */ @Test public void testVectorAnimation() throws ClassNotFoundException { // Create the layout pull parser. LayoutPullParser parser = createLayoutPullParser("indeterminate_progressbar.xml"); // Create LayoutLibCallback. LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); layoutLibCallback.initResources(); SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, RenderingMode.V_SCROLL, 22); renderAndVerify(params, "animated_vector.png", TimeUnit.SECONDS.toNanos(2)); parser = createLayoutPullParser("indeterminate_progressbar.xml"); params = getSessionParams(parser, ConfigGenerator.NEXUS_5, layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, RenderingMode.V_SCROLL, 22); renderAndVerify(params, "animated_vector_1.png", TimeUnit.SECONDS.toNanos(3)); } /** * Test a vector drawable that uses trimStart and trimEnd. It also tests all the primitives * for vector drawables (lines, moves and cubic and quadratic curves). */ @Test public void testVectorDrawable() throws ClassNotFoundException { // Create the layout pull parser. LayoutPullParser parser = createLayoutPullParser("vector_drawable.xml"); // Create LayoutLibCallback. LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); layoutLibCallback.initResources(); SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, RenderingMode.V_SCROLL, 22); renderAndVerify(params, "vector_drawable.png", TimeUnit.SECONDS.toNanos(2)); } /** Test activity.xml */ @Test public void testScrolling() throws ClassNotFoundException { // Create the layout pull parser. LayoutPullParser parser = createLayoutPullParser("scrolled.xml"); // Create LayoutLibCallback. LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); layoutLibCallback.initResources(); SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, RenderingMode.V_SCROLL, 22); params.setForceNoDecor(); params.setExtendedViewInfoMode(true); RenderResult result = renderAndVerify(params, "scrolled.png"); assertNotNull(result); assertTrue(result.getResult().isSuccess()); ViewInfo rootLayout = result.getRootViews().get(0); // Check the first box in the main LinearLayout assertEquals(-90, rootLayout.getChildren().get(0).getTop()); assertEquals(-30, rootLayout.getChildren().get(0).getLeft()); assertEquals(90, rootLayout.getChildren().get(0).getBottom()); assertEquals(150, rootLayout.getChildren().get(0).getRight()); // Check the first box within the nested LinearLayout assertEquals(-450, rootLayout.getChildren().get(5).getChildren().get(0).getTop()); assertEquals(90, rootLayout.getChildren().get(5).getChildren().get(0).getLeft()); assertEquals(-270, rootLayout.getChildren().get(5).getChildren().get(0).getBottom()); assertEquals(690, rootLayout.getChildren().get(5).getChildren().get(0).getRight()); } @Test public void testGetResourceNameVariants() throws Exception { // Setup SessionParams params = createSessionParams("", ConfigGenerator.NEXUS_4); AssetManager assetManager = AssetManager.getSystem(); DisplayMetrics metrics = new DisplayMetrics(); Configuration configuration = RenderAction.getConfiguration(params); Resources resources = new Resources(assetManager, metrics, configuration); resources.mLayoutlibCallback = params.getLayoutlibCallback(); resources.mContext = new BridgeContext(params.getProjectKey(), metrics, params.getResources(), params.getAssets(), params.getLayoutlibCallback(), configuration, params.getTargetSdkVersion(), params.isRtlSupported()); // Test assertEquals("android:style/ButtonBar", resources.getResourceName(android.R.style.ButtonBar)); assertEquals("android", resources.getResourcePackageName(android.R.style.ButtonBar)); assertEquals("ButtonBar", resources.getResourceEntryName(android.R.style.ButtonBar)); assertEquals("style", resources.getResourceTypeName(android.R.style.ButtonBar)); int id = resources.mLayoutlibCallback.getResourceId(ResourceType.STRING, "app_name"); assertEquals("com.android.layoutlib.test.myapplication:string/app_name", resources.getResourceName(id)); assertEquals("com.android.layoutlib.test.myapplication", resources.getResourcePackageName(id)); assertEquals("string", resources.getResourceTypeName(id)); assertEquals("app_name", resources.getResourceEntryName(id)); } @NonNull private LayoutPullParser createLayoutPullParser(String layoutPath) { return new LayoutPullParser(APP_TEST_RES + "/layout/" + layoutPath); } /** * Create a new rendering session and test that rendering the given layout doesn't throw any * exceptions and matches the provided image. *

* If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates * how far in the future is. */ @Nullable private RenderResult renderAndVerify(SessionParams params, String goldenFileName, long frameTimeNanos) throws ClassNotFoundException { // TODO: Set up action bar handler properly to test menu rendering. // Create session params. RenderSession session = sBridge.createSession(params); if (frameTimeNanos != -1) { session.setElapsedFrameTimeNanos(frameTimeNanos); } if (!session.getResult().isSuccess()) { getLogger().error(session.getResult().getException(), session.getResult().getErrorMessage()); } // Render the session with a timeout of 50s. Result renderResult = session.render(50000); if (!renderResult.isSuccess()) { getLogger().error(session.getResult().getException(), session.getResult().getErrorMessage()); } try { String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenFileName; ImageUtils.requireSimilar(goldenImagePath, session.getImage()); return RenderResult.getFromSession(session); } catch (IOException e) { getLogger().error(e, e.getMessage()); } finally { session.dispose(); } return null; } /** * Create a new rendering session and test that rendering the given layout doesn't throw any * exceptions and matches the provided image. */ @Nullable private RenderResult renderAndVerify(SessionParams params, String goldenFileName) throws ClassNotFoundException { return renderAndVerify(params, goldenFileName, -1); } /** * Create a new rendering session and test that rendering the given layout on nexus 5 * doesn't throw any exceptions and matches the provided image. */ @Nullable private RenderResult renderAndVerify(String layoutFileName, String goldenFileName) throws ClassNotFoundException { return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5); } /** * Create a new rendering session and test that rendering the given layout on given device * doesn't throw any exceptions and matches the provided image. */ @Nullable private RenderResult renderAndVerify(String layoutFileName, String goldenFileName, ConfigGenerator deviceConfig) throws ClassNotFoundException { SessionParams params = createSessionParams(layoutFileName, deviceConfig); return renderAndVerify(params, goldenFileName); } private SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig) throws ClassNotFoundException { // Create the layout pull parser. LayoutPullParser parser = createLayoutPullParser(layoutFileName); // Create LayoutLibCallback. LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); layoutLibCallback.initResources(); // TODO: Set up action bar handler properly to test menu rendering. // Create session params. return getSessionParams(parser, deviceConfig, layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22); } /** * Uses Theme.Material and Target sdk version as 22. */ private SessionParams getSessionParams(LayoutPullParser layoutParser, ConfigGenerator configGenerator, LayoutLibTestCallback layoutLibCallback, String themeName, boolean isProjectTheme, RenderingMode renderingMode, int targetSdk) { FolderConfiguration config = configGenerator.getFolderConfig(); ResourceResolver resourceResolver = ResourceResolver.create(sProjectResources.getConfiguredResources(config), sFrameworkRepo.getConfiguredResources(config), themeName, isProjectTheme); SessionParams sessionParams = new SessionParams( layoutParser, renderingMode, null /*used for caching*/, configGenerator.getHardwareConfig(), resourceResolver, layoutLibCallback, 0, targetSdk, getLayoutLog()); sessionParams.setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true); return sessionParams; } private static LayoutLog getLayoutLog() { if (sLayoutLibLog == null) { sLayoutLibLog = new LayoutLog() { @Override public void warning(String tag, String message, Object data) { System.out.println("Warning " + tag + ": " + message); failWithMsg(message); } @Override public void fidelityWarning(@Nullable String tag, String message, Throwable throwable, Object data) { System.out.println("FidelityWarning " + tag + ": " + message); if (throwable != null) { throwable.printStackTrace(); } failWithMsg(message == null ? "" : message); } @Override public void error(String tag, String message, Object data) { System.out.println("Error " + tag + ": " + message); failWithMsg(message); } @Override public void error(String tag, String message, Throwable throwable, Object data) { System.out.println("Error " + tag + ": " + message); if (throwable != null) { throwable.printStackTrace(); } failWithMsg(message); } }; } return sLayoutLibLog; } private static ILogger getLogger() { if (sLogger == null) { sLogger = new ILogger() { @Override public void error(Throwable t, @Nullable String msgFormat, Object... args) { if (t != null) { t.printStackTrace(); } failWithMsg(msgFormat == null ? "" : msgFormat, args); } @Override public void warning(@NonNull String msgFormat, Object... args) { failWithMsg(msgFormat, args); } @Override public void info(@NonNull String msgFormat, Object... args) { // pass. } @Override public void verbose(@NonNull String msgFormat, Object... args) { // pass. } }; } return sLogger; } private static void failWithMsg(@NonNull String msgFormat, Object... args) { sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args)); } }