Java tutorial
/* * Copyright 2012-2013 eBay Software Foundation and selendroid committers. * * 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 io.selendroid.builder; import io.selendroid.SelendroidConfiguration; import io.selendroid.android.AndroidApp; import io.selendroid.android.AndroidSdk; import io.selendroid.android.JavaSdk; import io.selendroid.android.impl.DefaultAndroidApp; import io.selendroid.exceptions.AndroidSdkException; import io.selendroid.exceptions.SelendroidException; import io.selendroid.exceptions.ShellCommandException; import io.selendroid.io.ShellCommand; import io.selendroid.server.model.SelendroidStandaloneDriver; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.exec.CommandLine; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; public class SelendroidServerBuilder { public static final String SELENDROID_TEST_APP_PACKAGE = "io.selendroid.testapp"; private static final Logger log = Logger.getLogger(SelendroidServerBuilder.class.getName()); public static final String SELENDROID_FINAL_NAME = "selendroid-server.apk"; public static final String PREBUILD_SELENDROID_SERVER_PATH_PREFIX = "/prebuild/selendroid-server-"; public static final String ANDROID_APPLICATION_XML_TEMPLATE = "/AndroidManifest.xml"; public static final String ICON = "android:icon=\"@drawable/selenium_icon\""; private String selendroidPrebuildServerPath = null; private String selendroidApplicationXmlTemplate = null; private AndroidApp selendroidServer = null; private AndroidApp applicationUnderTest = null; private SelendroidConfiguration serverConfiguration = null; /** * FOR TESTING ONLY */ /* package */ SelendroidServerBuilder(String selendroidPrebuildServerPath, String selendroidApplicationXmlTemplate) { this.selendroidPrebuildServerPath = selendroidPrebuildServerPath; this.selendroidApplicationXmlTemplate = selendroidApplicationXmlTemplate; } public SelendroidServerBuilder() { this(null); } public SelendroidServerBuilder(SelendroidConfiguration serverConfiguration) { this.selendroidPrebuildServerPath = PREBUILD_SELENDROID_SERVER_PATH_PREFIX + getJarVersionNumber() + ".apk"; this.selendroidApplicationXmlTemplate = ANDROID_APPLICATION_XML_TEMPLATE; this.serverConfiguration = serverConfiguration; } /* package */void init(AndroidApp aut) throws IOException, ShellCommandException { applicationUnderTest = aut; File customizedServer = File.createTempFile("selendroid-server", ".apk"); log.info("Creating customized Selendroid-server: " + customizedServer.getAbsolutePath()); InputStream is = getResourceAsStream(selendroidPrebuildServerPath); IOUtils.copy(is, new FileOutputStream(customizedServer)); IOUtils.closeQuietly(is); selendroidServer = new DefaultAndroidApp(customizedServer); } public AndroidApp createSelendroidServer(AndroidApp aut) throws IOException, ShellCommandException, AndroidSdkException { log.info("create SelendroidServer for apk: " + aut.getAbsolutePath()); init(aut); cleanUpPrebuildServer(); File selendroidServer = createAndAddCustomizedAndroidManifestToSelendroidServer(); File outputFile = new File(FileUtils.getTempDirectory(), String.format("selendroid-server-%s-%s.apk", applicationUnderTest.getBasePackage(), getJarVersionNumber())); return signTestServer(selendroidServer, outputFile); } private void deleteFileFromAppSilently(AndroidApp app, String file) throws AndroidSdkException { if (app == null) { throw new IllegalArgumentException("Required parameter 'app' is null."); } if (file == null || file.isEmpty()) { throw new IllegalArgumentException("Required parameter 'file' is null or empty."); } try { app.deleteFileFromWithinApk(file); } catch (ShellCommandException e) { // don't care, can happen if file does not exist } } public AndroidApp resignApp(File appFile) throws ShellCommandException, AndroidSdkException { AndroidApp app = new DefaultAndroidApp(appFile); // Delete existing certificates deleteFileFromAppSilently(app, "META-INF/MANIFEST.MF"); deleteFileFromAppSilently(app, "META-INF/CERT.RSA"); deleteFileFromAppSilently(app, "META-INF/CERT.SF"); deleteFileFromAppSilently(app, "META-INF/ANDROIDD.SF"); deleteFileFromAppSilently(app, "META-INF/ANDROIDD.RSA"); deleteFileFromAppSilently(app, "META-INF/NDKEYSTO.SF"); deleteFileFromAppSilently(app, "META-INF/NDKEYSTO.RSA"); File outputFile = new File(appFile.getParentFile(), "resigned-" + appFile.getName()); return signTestServer(appFile, outputFile); } /* package */File createAndAddCustomizedAndroidManifestToSelendroidServer() throws IOException, ShellCommandException, AndroidSdkException { String targetPackageName = applicationUnderTest.getBasePackage(); File tempdir = new File(FileUtils.getTempDirectoryPath() + File.separatorChar + targetPackageName + System.currentTimeMillis()); if (!tempdir.exists()) { tempdir.mkdirs(); } File customizedManifest = new File(tempdir, "AndroidManifest.xml"); log.info("Adding target package '" + targetPackageName + "' to " + customizedManifest.getAbsolutePath()); // add target package InputStream inputStream = getResourceAsStream(selendroidApplicationXmlTemplate); if (inputStream == null) { throw new SelendroidException("AndroidApplication.xml template file was not found."); } String content = IOUtils.toString(inputStream, Charset.defaultCharset().displayName()); // find the first occurance of "package" and appending the targetpackagename to begining int i = content.toLowerCase().indexOf("package"); int cnt = 0; for (; i < content.length(); i++) { if (content.charAt(i) == '\"') { cnt++; } if (cnt == 2) { break; } } content = content.substring(0, i) + "." + targetPackageName + content.substring(i); log.info("Final Manifest File:\n" + content); content = content.replaceAll(SELENDROID_TEST_APP_PACKAGE, targetPackageName); // Seems like this needs to be done if (content.contains(ICON)) { content = content.replaceAll(ICON, ""); } OutputStream outputStream = new FileOutputStream(customizedManifest); IOUtils.write(content, outputStream, Charset.defaultCharset().displayName()); IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(outputStream); // adding the xml to an empty apk CommandLine createManifestApk = new CommandLine(AndroidSdk.aapt()); createManifestApk.addArgument("package", false); createManifestApk.addArgument("-M", false); createManifestApk.addArgument(customizedManifest.getAbsolutePath(), false); createManifestApk.addArgument("-I", false); createManifestApk.addArgument(AndroidSdk.androidJar(), false); createManifestApk.addArgument("-F", false); createManifestApk.addArgument(tempdir.getAbsolutePath() + File.separatorChar + "manifest.apk", false); createManifestApk.addArgument("-f", false); log.info(ShellCommand.exec(createManifestApk, 20000L)); ZipFile manifestApk = new ZipFile( new File(tempdir.getAbsolutePath() + File.separatorChar + "manifest.apk")); ZipArchiveEntry binaryManifestXml = manifestApk.getEntry("AndroidManifest.xml"); File finalSelendroidServerFile = new File(tempdir.getAbsolutePath() + "selendroid-server.apk"); ZipArchiveOutputStream finalSelendroidServer = new ZipArchiveOutputStream(finalSelendroidServerFile); finalSelendroidServer.putArchiveEntry(binaryManifestXml); IOUtils.copy(manifestApk.getInputStream(binaryManifestXml), finalSelendroidServer); ZipFile selendroidPrebuildApk = new ZipFile(selendroidServer.getAbsolutePath()); Enumeration<ZipArchiveEntry> entries = selendroidPrebuildApk.getEntries(); for (; entries.hasMoreElements();) { ZipArchiveEntry dd = entries.nextElement(); finalSelendroidServer.putArchiveEntry(dd); IOUtils.copy(selendroidPrebuildApk.getInputStream(dd), finalSelendroidServer); } finalSelendroidServer.closeArchiveEntry(); finalSelendroidServer.close(); manifestApk.close(); log.info("file: " + finalSelendroidServerFile.getAbsolutePath()); return finalSelendroidServerFile; } /* package */AndroidApp signTestServer(File customSelendroidServer, File outputFileName) throws ShellCommandException, AndroidSdkException { if (outputFileName == null) { throw new IllegalArgumentException("outputFileName parameter is null."); } File androidKeyStore = androidDebugKeystore(); if (androidKeyStore.isFile() == false) { // create a new keystore CommandLine commandline = new CommandLine(JavaSdk.keytool()); commandline.addArgument("-genkey", false); commandline.addArgument("-v", false); commandline.addArgument("-keystore", false); commandline.addArgument(androidKeyStore.toString(), false); commandline.addArgument("-storepass", false); commandline.addArgument("android", false); commandline.addArgument("-alias", false); commandline.addArgument("androiddebugkey", false); commandline.addArgument("-keypass", false); commandline.addArgument("android", false); commandline.addArgument("-dname", false); commandline.addArgument("CN=Android Debug,O=Android,C=US", false); commandline.addArgument("-storetype", false); commandline.addArgument("JKS", false); commandline.addArgument("-sigalg", false); commandline.addArgument("MD5withRSA", false); commandline.addArgument("-keyalg", false); commandline.addArgument("RSA", false); commandline.addArgument("-validity", false); commandline.addArgument("9999", false); String output = ShellCommand.exec(commandline, 20000); log.info("A new keystore has been created: " + output); } // Sign the jar CommandLine commandline = new CommandLine(JavaSdk.jarsigner()); commandline.addArgument("-sigalg", false); commandline.addArgument("MD5withRSA", false); commandline.addArgument("-digestalg", false); commandline.addArgument("SHA1", false); commandline.addArgument("-signedjar", false); commandline.addArgument(outputFileName.getAbsolutePath(), false); commandline.addArgument("-storepass", false); commandline.addArgument("android", false); commandline.addArgument("-keystore", false); commandline.addArgument(androidKeyStore.toString(), false); commandline.addArgument(customSelendroidServer.getAbsolutePath(), false); commandline.addArgument("androiddebugkey", false); String output = ShellCommand.exec(commandline, 20000); if (log.isLoggable(Level.INFO)) { log.info("App signing output: " + output); } log.info("The app has been signed: " + outputFileName.getAbsolutePath()); return new DefaultAndroidApp(outputFileName); } private File androidDebugKeystore() { if (serverConfiguration == null || serverConfiguration.getKeystore() == null) { return new File(FileUtils.getUserDirectory(), File.separatorChar + ".android" + File.separatorChar + "debug.keystore"); } else { return new File(serverConfiguration.getKeystore()); } } /** * Cleans the selendroid server by removing certificates and manifest file. * * Precondition: {@link #init(AndroidApp)} must be called upfront for initialization * * @throws ShellCommandException * @throws AndroidSdkException */ /* package */void cleanUpPrebuildServer() throws ShellCommandException, AndroidSdkException { selendroidServer.deleteFileFromWithinApk("META-INF/CERT.RSA"); selendroidServer.deleteFileFromWithinApk("META-INF/CERT.SF"); selendroidServer.deleteFileFromWithinApk("AndroidManifest.xml"); } /** * for testing only */ /* package */AndroidApp getSelendroidServer() { return selendroidServer; } /** * for testing only */ /* package */AndroidApp getApplicationUnderTest() { return applicationUnderTest; } /** * Loads resources as stream and the main reason for having the method is because it can be use * while testing and in production for loading files from within jar file. * * @param resource The resource to load. * @return The input stream of the resource. * @throws SelendroidException if resource was not found. */ private InputStream getResourceAsStream(String resource) { InputStream is = null; is = getClass().getResourceAsStream(resource); // switch needed for testability if (is == null) { try { is = new FileInputStream(new File(resource)); } catch (FileNotFoundException e) { // do nothing } } if (is == null) { throw new SelendroidException("The resource '" + resource + "' was not found."); } return is; } // TODO this should go into a utility method public static String getJarVersionNumber() { Class clazz = SelendroidStandaloneDriver.class; String className = clazz.getSimpleName() + ".class"; String classPath = clazz.getResource(className).toString(); if (!classPath.startsWith("jar")) { // Class not from JAR return "dev"; } String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF"; Manifest manifest = null; try { manifest = new Manifest(new URL(manifestPath).openStream()); } catch (Exception e) { return ""; } Attributes attr = manifest.getMainAttributes(); String value = attr.getValue("version"); return value; } }