Main.java revision cf935728897e9f208e250d3bb57296c84cfa0ef9
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.layoutlib.bridge.intensive; 18 19import com.android.ide.common.rendering.api.LayoutLog; 20import com.android.ide.common.rendering.api.RenderSession; 21import com.android.ide.common.rendering.api.Result; 22import com.android.ide.common.rendering.api.SessionParams; 23import com.android.ide.common.rendering.api.SessionParams.RenderingMode; 24import com.android.ide.common.resources.FrameworkResources; 25import com.android.ide.common.resources.ResourceItem; 26import com.android.ide.common.resources.ResourceRepository; 27import com.android.ide.common.resources.ResourceResolver; 28import com.android.ide.common.resources.configuration.FolderConfiguration; 29import com.android.io.FolderWrapper; 30import com.android.layoutlib.bridge.Bridge; 31import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator; 32import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback; 33import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser; 34import com.android.resources.Density; 35import com.android.resources.Navigation; 36import com.android.utils.ILogger; 37 38import org.junit.AfterClass; 39import org.junit.BeforeClass; 40import org.junit.Test; 41 42import android.annotation.NonNull; 43import android.annotation.Nullable; 44 45import java.io.File; 46import java.io.FileFilter; 47import java.io.IOException; 48import java.net.URL; 49import java.util.Arrays; 50import java.util.Comparator; 51import java.util.Map; 52import java.util.concurrent.TimeUnit; 53 54import static org.junit.Assert.fail; 55 56/** 57 * This is a set of tests that loads all the framework resources and a project checked in this 58 * test's resources. The main dependencies 59 * are: 60 * 1. Fonts directory. 61 * 2. Framework Resources. 62 * 3. App resources. 63 * 4. build.prop file 64 * 65 * These are configured by two variables set in the system properties. 66 * 67 * 1. platform.dir: This is the directory for the current platform in the built SDK 68 * (.../sdk/platforms/android-<version>). 69 * 70 * The fonts are platform.dir/data/fonts. 71 * The Framework resources are platform.dir/data/res. 72 * build.prop is at platform.dir/build.prop. 73 * 74 * 2. test_res.dir: This is the directory for the resources of the test. If not specified, this 75 * falls back to getClass().getProtectionDomain().getCodeSource().getLocation() 76 * 77 * The app resources are at: test_res.dir/testApp/MyApplication/app/src/main/res 78 */ 79public class Main { 80 81 private static final String PLATFORM_DIR_PROPERTY = "platform.dir"; 82 private static final String RESOURCE_DIR_PROPERTY = "test_res.dir"; 83 84 private static final String PLATFORM_DIR; 85 private static final String TEST_RES_DIR; 86 /** Location of the app to test inside {@link #TEST_RES_DIR}*/ 87 private static final String APP_TEST_DIR = "/testApp/MyApplication"; 88 /** Location of the app's res dir inside {@link #TEST_RES_DIR}*/ 89 private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res"; 90 91 private static LayoutLog sLayoutLibLog; 92 private static FrameworkResources sFrameworkRepo; 93 private static ResourceRepository sProjectResources; 94 private static ILogger sLogger; 95 private static Bridge sBridge; 96 97 static { 98 // Test that System Properties are properly set. 99 PLATFORM_DIR = getPlatformDir(); 100 if (PLATFORM_DIR == null) { 101 fail(String.format("System Property %1$s not properly set. The value is %2$s", 102 PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY))); 103 } 104 105 TEST_RES_DIR = getTestResDir(); 106 if (TEST_RES_DIR == null) { 107 fail(String.format("System property %1$s.dir not properly set. The value is %2$s", 108 RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY))); 109 } 110 } 111 112 private static String getPlatformDir() { 113 String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY); 114 if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) { 115 return platformDir; 116 } 117 // System Property not set. Try to find the directory in the build directory. 118 String androidHostOut = System.getenv("ANDROID_HOST_OUT"); 119 if (androidHostOut != null) { 120 platformDir = getPlatformDirFromHostOut(new File(androidHostOut)); 121 if (platformDir != null) { 122 return platformDir; 123 } 124 } 125 String workingDirString = System.getProperty("user.dir"); 126 File workingDir = new File(workingDirString); 127 // Test if workingDir is android checkout root. 128 platformDir = getPlatformDirFromRoot(workingDir); 129 if (platformDir != null) { 130 return platformDir; 131 } 132 133 // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge. 134 File currentDir = workingDir; 135 if (currentDir.getName().equalsIgnoreCase("bridge")) { 136 currentDir = currentDir.getParentFile(); 137 } 138 // Test if currentDir is platform/frameworks/base/tools/layoutlib. That is, root should be 139 // workingDir/../../../../ (4 levels up) 140 for (int i = 0; i < 4; i++) { 141 if (currentDir != null) { 142 currentDir = currentDir.getParentFile(); 143 } 144 } 145 return currentDir == null ? null : getPlatformDirFromRoot(currentDir); 146 } 147 148 private static String getPlatformDirFromRoot(File root) { 149 if (!root.isDirectory()) { 150 return null; 151 } 152 File out = new File(root, "out"); 153 if (!out.isDirectory()) { 154 return null; 155 } 156 File host = new File(out, "host"); 157 if (!host.isDirectory()) { 158 return null; 159 } 160 File[] hosts = host.listFiles(new FileFilter() { 161 @Override 162 public boolean accept(File path) { 163 return path.isDirectory() && (path.getName().startsWith("linux-") || path.getName() 164 .startsWith("darwin-")); 165 } 166 }); 167 for (File hostOut : hosts) { 168 String platformDir = getPlatformDirFromHostOut(hostOut); 169 if (platformDir != null) { 170 return platformDir; 171 } 172 } 173 return null; 174 } 175 176 private static String getPlatformDirFromHostOut(File out) { 177 if (!out.isDirectory()) { 178 return null; 179 } 180 File sdkDir = new File(out, "sdk"); 181 if (!sdkDir.isDirectory()) { 182 return null; 183 } 184 File[] sdkDirs = sdkDir.listFiles(new FileFilter() { 185 @Override 186 public boolean accept(File path) { 187 // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7) 188 return path.isDirectory() && path.getName().startsWith("sdk"); 189 } 190 }); 191 for (File dir : sdkDirs) { 192 String platformDir = getPlatformDirFromHostOutSdkSdk(dir); 193 if (platformDir != null) { 194 return platformDir; 195 } 196 } 197 return null; 198 } 199 200 private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) { 201 File[] possibleSdks = sdkDir.listFiles(new FileFilter() { 202 @Override 203 public boolean accept(File path) { 204 return path.isDirectory() && path.getName().contains("android-sdk"); 205 } 206 }); 207 for (File possibleSdk : possibleSdks) { 208 File platformsDir = new File(possibleSdk, "platforms"); 209 File[] platforms = platformsDir.listFiles(new FileFilter() { 210 @Override 211 public boolean accept(File path) { 212 return path.isDirectory() && path.getName().startsWith("android-"); 213 } 214 }); 215 if (platforms == null || platforms.length == 0) { 216 continue; 217 } 218 Arrays.sort(platforms, new Comparator<File>() { 219 // Codenames before ints. Higher APIs precede lower. 220 @Override 221 public int compare(File o1, File o2) { 222 final int MAX_VALUE = 1000; 223 String suffix1 = o1.getName().substring("android-".length()); 224 String suffix2 = o2.getName().substring("android-".length()); 225 int suff1, suff2; 226 try { 227 suff1 = Integer.parseInt(suffix1); 228 } catch (NumberFormatException e) { 229 suff1 = MAX_VALUE; 230 } 231 try { 232 suff2 = Integer.parseInt(suffix2); 233 } catch (NumberFormatException e) { 234 suff2 = MAX_VALUE; 235 } 236 if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) { 237 return suff2 - suff1; 238 } 239 return suffix2.compareTo(suffix1); 240 } 241 }); 242 return platforms[0].getAbsolutePath(); 243 } 244 return null; 245 } 246 247 private static String getTestResDir() { 248 String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY); 249 if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) { 250 return resourceDir; 251 } 252 // TEST_RES_DIR not explicitly set. Fallback to the class's source location. 253 try { 254 URL location = Main.class.getProtectionDomain().getCodeSource().getLocation(); 255 return new File(location.getPath()).exists() ? location.getPath() : null; 256 } catch (NullPointerException e) { 257 // Prevent a lot of null checks by just catching the exception. 258 return null; 259 } 260 } 261 /** 262 * Initialize the bridge and the resource maps. 263 */ 264 @BeforeClass 265 public static void setUp() { 266 File data_dir = new File(PLATFORM_DIR, "data"); 267 File res = new File(data_dir, "res"); 268 sFrameworkRepo = new FrameworkResources(new FolderWrapper(res)); 269 sFrameworkRepo.loadResources(); 270 sFrameworkRepo.loadPublicResources(getLogger()); 271 272 sProjectResources = 273 new ResourceRepository(new FolderWrapper(TEST_RES_DIR + APP_TEST_RES), false) { 274 @NonNull 275 @Override 276 protected ResourceItem createResourceItem(@NonNull String name) { 277 return new ResourceItem(name); 278 } 279 }; 280 sProjectResources.loadResources(); 281 282 File fontLocation = new File(data_dir, "fonts"); 283 File buildProp = new File(PLATFORM_DIR, "build.prop"); 284 File attrs = new File(res, "values" + File.separator + "attrs.xml"); 285 sBridge = new Bridge(); 286 sBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation, 287 ConfigGenerator.getEnumMap(attrs), getLayoutLog()); 288 } 289 290 /** Test activity.xml */ 291 @Test 292 public void testActivity() throws ClassNotFoundException { 293 renderAndVerify("activity.xml", "activity.png"); 294 295 } 296 297 /** Test allwidgets.xml */ 298 @Test 299 public void testAllWidgets() throws ClassNotFoundException { 300 renderAndVerify("allwidgets.xml", "allwidgets.png"); 301 } 302 303 @Test 304 public void testArrayCheck() throws ClassNotFoundException { 305 renderAndVerify("array_check.xml", "array_check.png"); 306 } 307 308 @Test 309 public void testAllWidgetsTablet() throws ClassNotFoundException { 310 renderAndVerify("allwidgets.xml", "allwidgets_tab.png", ConfigGenerator.NEXUS_7_2012); 311 } 312 313 @AfterClass 314 public static void tearDown() { 315 sLayoutLibLog = null; 316 sFrameworkRepo = null; 317 sProjectResources = null; 318 sLogger = null; 319 sBridge = null; 320 } 321 322 /** Test expand_layout.xml */ 323 @Test 324 public void testExpand() throws ClassNotFoundException { 325 // Create the layout pull parser. 326 LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + 327 "expand_vert_layout.xml"); 328 // Create LayoutLibCallback. 329 LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); 330 layoutLibCallback.initResources(); 331 332 ConfigGenerator customConfigGenerator = new ConfigGenerator() 333 .setScreenWidth(300) 334 .setScreenHeight(20) 335 .setDensity(Density.XHIGH) 336 .setNavigation(Navigation.NONAV); 337 338 SessionParams params = getSessionParams(parser, customConfigGenerator, 339 layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, 340 RenderingMode.V_SCROLL, 22); 341 342 renderAndVerify(params, "expand_vert_layout.png"); 343 344 customConfigGenerator = new ConfigGenerator() 345 .setScreenWidth(20) 346 .setScreenHeight(300) 347 .setDensity(Density.XHIGH) 348 .setNavigation(Navigation.NONAV); 349 parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + 350 "expand_horz_layout.xml"); 351 params = getSessionParams(parser, customConfigGenerator, 352 layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, 353 RenderingMode.H_SCROLL, 22); 354 355 renderAndVerify(params, "expand_horz_layout.png"); 356 } 357 358 /** Test indeterminate_progressbar.xml */ 359 @Test 360 public void testVectorAnimation() throws ClassNotFoundException { 361 // Create the layout pull parser. 362 LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + 363 "indeterminate_progressbar.xml"); 364 // Create LayoutLibCallback. 365 LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); 366 layoutLibCallback.initResources(); 367 368 SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, 369 layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, 370 RenderingMode.V_SCROLL, 22); 371 372 renderAndVerify(params, "animated_vector.png", TimeUnit.SECONDS.toNanos(2)); 373 374 parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + 375 "indeterminate_progressbar.xml"); 376 params = getSessionParams(parser, ConfigGenerator.NEXUS_5, 377 layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, 378 RenderingMode.V_SCROLL, 22); 379 renderAndVerify(params, "animated_vector_1.png", TimeUnit.SECONDS.toNanos(3)); 380 } 381 382 /** 383 * Test a vector drawable that uses trimStart and trimEnd. It also tests all the primitives 384 * for vector drawables (lines, moves and cubic and quadratic curves). 385 */ 386 @Test 387 public void testVectorDrawable() throws ClassNotFoundException { 388 // Create the layout pull parser. 389 LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + 390 "vector_drawable.xml"); 391 // Create LayoutLibCallback. 392 LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); 393 layoutLibCallback.initResources(); 394 395 SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, 396 layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, 397 RenderingMode.V_SCROLL, 22); 398 399 renderAndVerify(params, "vector_drawable.png", TimeUnit.SECONDS.toNanos(2)); 400 } 401 402 /** 403 * Create a new rendering session and test that rendering the given layout doesn't throw any 404 * exceptions and matches the provided image. 405 * <p> 406 * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates 407 * how far in the future is. 408 */ 409 private void renderAndVerify(SessionParams params, String goldenFileName, long frameTimeNanos) 410 throws ClassNotFoundException { 411 // TODO: Set up action bar handler properly to test menu rendering. 412 // Create session params. 413 RenderSession session = sBridge.createSession(params); 414 415 if (frameTimeNanos != -1) { 416 session.setElapsedFrameTimeNanos(frameTimeNanos); 417 } 418 419 if (!session.getResult().isSuccess()) { 420 getLogger().error(session.getResult().getException(), 421 session.getResult().getErrorMessage()); 422 } 423 // Render the session with a timeout of 50s. 424 Result renderResult = session.render(50000); 425 if (!renderResult.isSuccess()) { 426 getLogger().error(session.getResult().getException(), 427 session.getResult().getErrorMessage()); 428 } 429 try { 430 String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenFileName; 431 ImageUtils.requireSimilar(goldenImagePath, session.getImage()); 432 } catch (IOException e) { 433 getLogger().error(e, e.getMessage()); 434 } 435 } 436 437 /** 438 * Create a new rendering session and test that rendering the given layout doesn't throw any 439 * exceptions and matches the provided image. 440 */ 441 private void renderAndVerify(SessionParams params, String goldenFileName) 442 throws ClassNotFoundException { 443 renderAndVerify(params, goldenFileName, -1); 444 } 445 446 /** 447 * Create a new rendering session and test that rendering the given layout on nexus 5 448 * doesn't throw any exceptions and matches the provided image. 449 */ 450 private void renderAndVerify(String layoutFileName, String goldenFileName) 451 throws ClassNotFoundException { 452 renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5); 453 } 454 455 /** 456 * Create a new rendering session and test that rendering the given layout on given device 457 * doesn't throw any exceptions and matches the provided image. 458 */ 459 private void renderAndVerify(String layoutFileName, String goldenFileName, 460 ConfigGenerator deviceConfig) 461 throws ClassNotFoundException { 462 // Create the layout pull parser. 463 LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + layoutFileName); 464 // Create LayoutLibCallback. 465 LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); 466 layoutLibCallback.initResources(); 467 // TODO: Set up action bar handler properly to test menu rendering. 468 // Create session params. 469 SessionParams params = getSessionParams(parser, deviceConfig, 470 layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22); 471 renderAndVerify(params, goldenFileName); 472 } 473 474 /** 475 * Uses Theme.Material and Target sdk version as 22. 476 */ 477 private SessionParams getSessionParams(LayoutPullParser layoutParser, 478 ConfigGenerator configGenerator, LayoutLibTestCallback layoutLibCallback, 479 String themeName, boolean isProjectTheme, RenderingMode renderingMode, int targetSdk) { 480 FolderConfiguration config = configGenerator.getFolderConfig(); 481 ResourceResolver resourceResolver = 482 ResourceResolver.create(sProjectResources.getConfiguredResources(config), 483 sFrameworkRepo.getConfiguredResources(config), 484 themeName, isProjectTheme); 485 486 return new SessionParams( 487 layoutParser, 488 renderingMode, 489 null /*used for caching*/, 490 configGenerator.getHardwareConfig(), 491 resourceResolver, 492 layoutLibCallback, 493 0, 494 targetSdk, 495 getLayoutLog()); 496 } 497 498 private static LayoutLog getLayoutLog() { 499 if (sLayoutLibLog == null) { 500 sLayoutLibLog = new LayoutLog() { 501 @Override 502 public void warning(String tag, String message, Object data) { 503 System.out.println("Warning " + tag + ": " + message); 504 failWithMsg(message); 505 } 506 507 @Override 508 public void fidelityWarning(@Nullable String tag, String message, 509 Throwable throwable, Object data) { 510 511 System.out.println("FidelityWarning " + tag + ": " + message); 512 if (throwable != null) { 513 throwable.printStackTrace(); 514 } 515 failWithMsg(message == null ? "" : message); 516 } 517 518 @Override 519 public void error(String tag, String message, Object data) { 520 System.out.println("Error " + tag + ": " + message); 521 failWithMsg(message); 522 } 523 524 @Override 525 public void error(String tag, String message, Throwable throwable, Object data) { 526 System.out.println("Error " + tag + ": " + message); 527 if (throwable != null) { 528 throwable.printStackTrace(); 529 } 530 failWithMsg(message); 531 } 532 }; 533 } 534 return sLayoutLibLog; 535 } 536 537 private static ILogger getLogger() { 538 if (sLogger == null) { 539 sLogger = new ILogger() { 540 @Override 541 public void error(Throwable t, @Nullable String msgFormat, Object... args) { 542 if (t != null) { 543 t.printStackTrace(); 544 } 545 failWithMsg(msgFormat == null ? "" : msgFormat, args); 546 } 547 548 @Override 549 public void warning(@NonNull String msgFormat, Object... args) { 550 failWithMsg(msgFormat, args); 551 } 552 553 @Override 554 public void info(@NonNull String msgFormat, Object... args) { 555 // pass. 556 } 557 558 @Override 559 public void verbose(@NonNull String msgFormat, Object... args) { 560 // pass. 561 } 562 }; 563 } 564 return sLogger; 565 } 566 567 private static void failWithMsg(@NonNull String msgFormat, Object... args) { 568 fail(args == null ? "" : String.format(msgFormat, args)); 569 } 570} 571