/* * Copyright (C) 2009 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 dalvik.runner; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Logger; /** * Runs a test in the context of an android.app.Activity on a device */ final class ActivityMode extends Mode { private static final Logger logger = Logger.getLogger(ActivityMode.class.getName()); private static final String TEST_ACTIVITY_CLASS = "dalvik.runner.TestActivity"; ActivityMode(Integer debugPort, long timeoutSeconds, File sdkJar, PrintStream tee, File localTemp, boolean cleanBefore, boolean cleanAfter, File deviceRunnerDir) { super(new EnvironmentDevice(cleanBefore, cleanAfter, debugPort, localTemp, deviceRunnerDir), timeoutSeconds, sdkJar, tee); } private EnvironmentDevice getEnvironmentDevice() { return (EnvironmentDevice) environment; } @Override protected void prepare(Set<File> testRunnerJava, Classpath testRunnerClasspath) { testRunnerJava.add(new File("dalvik/libcore/tools/runner/lib/TestActivity.java")); super.prepare(testRunnerJava, testRunnerClasspath); } @Override protected void postCompileTestRunner() { } @Override protected void postCompileTest(TestRun testRun) { logger.fine("aapt and push " + testRun.getQualifiedName()); // Some things of note: // 1. we can't put multiple dex files in one apk // 2. we can't just give dex multiple jars with conflicting class names // 3. dex is slow if we give it too much to chew on // 4. dex can run out of memory if given too much to chew on // With that in mind, the APK packaging strategy is as follows: // 1. make an empty classes temporary directory // 2. add test runner classes // 3. find original jar test came from, add contents to classes // 4. add supported runner classes specified by finder // 5. add latest test classes to output // 6. dx to create a dex // 7. aapt the dex to create apk // 8. sign the apk // 9. install the apk File packagingDir = makePackagingDirectory(testRun); addTestRunnerClasses(packagingDir); List<File> found = new ArrayList<File>(); File originalTestJar = findOriginalTestJar(testRun); if (originalTestJar != null) { found.add(originalTestJar); } found.addAll(testRun.getRunnerClasspath().getElements()); extractJars(packagingDir, found); addTestClasses(testRun, packagingDir); File dex = createDex(testRun, packagingDir); File apkUnsigned = createApk(testRun, dex); File apkSigned = signApk(testRun, apkUnsigned); installApk(testRun, apkSigned); } private File makePackagingDirectory(TestRun testRun) { File packagingDir = new File(environment.testCompilationDir(testRun), "packaging"); new Rm().directoryTree(packagingDir); new Mkdir().mkdirs(packagingDir); return packagingDir; } private void addTestRunnerClasses(File packagingDir) { new Command("rsync", "-a", environment.testRunnerClassesDir() + "/", packagingDir + "/").execute(); } private File findOriginalTestJar(TestRun testRun) { String testClass = testRun.getTestClass(); String testFile = testClass.replace('.', '/') + ".class"; for (File element : testClasspath.getElements()) { try { JarFile jar = new JarFile(element); JarEntry jarEntry = jar.getJarEntry(testFile); if (jarEntry != null) { return element; } } catch (IOException e) { throw new RuntimeException( "Could not find element " + element + " of test class path " + testClasspath, e); } } return null; } private static void extractJars(File packagingDir, List<File> jars) { for (File jar : jars) { new Command.Builder() .args("unzip") .args("-q") .args("-o") .args(jar) .args("-d") .args(packagingDir).execute(); } new Rm().directoryTree(new File(packagingDir, "META-INF")); } private void addTestClasses(TestRun testRun, File packagingDir) { File testClassesDir = environment.testClassesDir(testRun); new Command("rsync", "-a", testClassesDir + "/", packagingDir + "/").execute(); } private File createDex (TestRun testRun, File packagingDir) { File testClassesDir = environment.testClassesDir(testRun); File dex = new File(testClassesDir + ".dex"); new Dx().dex(dex, Classpath.of(packagingDir)); return dex; } /** * According to android.content.pm.PackageParser, package name * "must have at least one '.' separator" Since the qualified name * may not contain a dot, we prefix containing one to ensure we * are compliant. */ private static String packageName(TestRun testRun) { return "DalvikRunner." + testRun.getQualifiedName(); } private File createApk (TestRun testRun, File dex) { String androidManifest = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + " package=\"" + packageName(testRun) + "\">\n" + " <uses-permission android:name=\"android.permission.INTERNET\" />\n" + " <application>\n" + " <activity android:name=\"" + TEST_ACTIVITY_CLASS + "\">\n" + " <intent-filter>\n" + " <action android:name=\"android.intent.action.MAIN\" />\n" + " <category android:name=\"android.intent.category.LAUNCHER\" />\n" + " </intent-filter>\n" + " </activity>\n" + " </application>\n" + "</manifest>\n"; File androidManifestFile = new File(environment.testCompilationDir(testRun), "AndroidManifest.xml"); try { FileOutputStream androidManifestOut = new FileOutputStream(androidManifestFile); androidManifestOut.write(androidManifest.getBytes("UTF-8")); androidManifestOut.close(); } catch (IOException e) { throw new RuntimeException("Problem writing " + androidManifestFile, e); } File testClassesDir = environment.testClassesDir(testRun); File apkUnsigned = new File(testClassesDir + ".apk.unsigned"); new Aapt().apk(apkUnsigned, androidManifestFile); new Aapt().add(apkUnsigned, dex); new Aapt().add(apkUnsigned, new File(testClassesDir, TestProperties.FILE)); return apkUnsigned; } private File signApk(TestRun testRun, File apkUnsigned) { File testClassesDir = environment.testClassesDir(testRun); File apkSigned = new File(testClassesDir, testRun.getQualifiedName() + ".apk"); // TODO: we should be able to work with a shipping SDK, not depend on out/... // TODO: we should be able to work without hardwired keys, not depend on build/... new Command.Builder() .args("java") .args("-jar") .args("out/host/linux-x86/framework/signapk.jar") .args("build/target/product/security/testkey.x509.pem") .args("build/target/product/security/testkey.pk8") .args(apkUnsigned) .args(apkSigned).execute(); new Rm().file(apkUnsigned); return apkSigned; } private void installApk(TestRun testRun, File apkSigned) { // install the local apk ona the device getEnvironmentDevice().adb.uninstall(packageName(testRun)); getEnvironmentDevice().adb.install(apkSigned); } @Override protected void fillInProperties(Properties properties, TestRun testRun) { super.fillInProperties(properties, testRun); properties.setProperty(TestProperties.DEVICE_RUNNER_DIR, getEnvironmentDevice().runnerDir.getPath()); } @Override protected List<String> runTestCommand(TestRun testRun) throws TimeoutException { new Command( "adb", "shell", "am", "start", "-a","android.intent.action.MAIN", "-n", (packageName(testRun) + "/" + TEST_ACTIVITY_CLASS)).executeWithTimeout(timeoutSeconds); File resultDir = new File(getEnvironmentDevice().runnerDir, testRun.getQualifiedName()); File resultFile = new File(resultDir, TestProperties.RESULT_FILE); getEnvironmentDevice().adb.waitForFile(resultFile, timeoutSeconds); return new Command.Builder() .args("adb", "shell", "cat", resultFile.getPath()) .tee(tee) .build().executeWithTimeout(timeoutSeconds); } @Override void cleanup(TestRun testRun) { super.cleanup(testRun); if (environment.cleanAfter) { getEnvironmentDevice().adb.uninstall(testRun.getQualifiedName()); } } }