/* * Copyright (C) 2009, 2010 Jayway AB * Copyright (C) 2007-2008 JVending Masa * * 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.jayway.maven.plugins.android; import org.apache.commons.jxpath.JXPathContext; import org.apache.commons.jxpath.JXPathNotFoundException; import org.apache.commons.jxpath.xml.DocumentContainer; import org.apache.commons.lang.StringUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.dependency.utils.resolvers.ArtifactsResolver; import org.apache.maven.plugin.dependency.utils.resolvers.DefaultArtifactsResolver; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.codehaus.plexus.util.DirectoryScanner; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import static org.apache.commons.lang.StringUtils.isBlank; /** * Contains common fields and methods for android mojos. * * @author hugo.josefson@jayway.com */ public abstract class AbstractAndroidMojo extends AbstractMojo { /** * The file extension used for the android package file. */ protected static final String ANDROID_PACKAGE_EXTENSTION = ".apk"; /** * The maven project. * * @parameter expression="${project}" * @required * @readonly */ protected MavenProject project; /** * The maven session. * * @parameter expression="${session}" * @required * @readonly */ protected MavenSession session; /** * The java sources directory. * * @parameter default-value="${project.build.sourceDirectory}" * @readonly */ protected File sourceDirectory; /** * The android resources directory. * * @parameter default-value="${project.basedir}/res" */ protected File resourceDirectory; /** * The android resources overlay directory. This will be overriden * by resourceOverlayDirectories if present. * * @parameter default-value="${project.basedir}/res-overlay" */ protected File resourceOverlayDirectory; /** * The android resources overlay directories. If this is specified, * the {@link #resourceOverlayDirectory} parameter will be ignored. * * @parameter */ protected File[] resourceOverlayDirectories; /** * The android assets directory. * * @parameter default-value="${project.basedir}/assets" */ protected File assetsDirectory; /** * The <code>AndroidManifest.xml</code> file. * * @parameter default-value="${project.basedir}/AndroidManifest.xml" */ protected File androidManifestFile; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies" * @readonly */ protected File extractedDependenciesDirectory; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/res" * @readonly */ protected File extractedDependenciesRes; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/assets" * @readonly */ protected File extractedDependenciesAssets; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/src/main/java" * @readonly */ protected File extractedDependenciesJavaSources; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/src/main/resources" * @readonly */ protected File extractedDependenciesJavaResources; /** * The combined resources directory. This will contain both the resources found in "res" as well as any resources contained in a apksources dependency. * * @parameter expression="${project.build.directory}/generated-sources/combined-resources/res" * @readonly */ protected File combinedRes; /** * Specifies which device to connect to, by serial number. Special values "usb" and "emulator" are also valid, for * selecting the only USB connected device or the only running emulator, respectively. * * @parameter expression="${android.device}" */ protected String device; /** * A selection of configurations to be included in the APK as a comma separated list. This will limit the configurations for a certain type. * For example, specifying <code>hdpi</code> will exclude all resource folders with the <code>mdpi</code> or <code>ldpi</code> * modifiers, but won't affect language or orientation modifiers. For more information about this option, look in the aapt command line help. * * @parameter expression="${android.configurations}" */ protected String configurations; /** * Decides whether the Apk should be generated or not. If set to false, dx and apkBuilder will not run. This is probably most * useful for a project used to generate apk sources to be inherited into another application project. * * @parameter expression="${android.generateApk}" default-value="true" */ protected boolean generateApk; /** * Used to look up Artifacts in the remote repository. * * @component */ protected org.apache.maven.artifact.resolver.ArtifactResolver artifactResolver; /** * Location of the local repository. * * @parameter expression="${localRepository}" * @readonly * @required */ protected org.apache.maven.artifact.repository.ArtifactRepository localRepository; /** * List of Remote Repositories used by the resolver * * @parameter expression="${project.remoteArtifactRepositories}" * @readonly * @required */ protected java.util.List remoteRepositories; /** * @component * @readonly * @required */ protected ArtifactFactory artifactFactory; /** * Maven ProjectHelper. * * @component * @readonly */ protected MavenProjectHelper projectHelper; /** * <p>The Android SDK to use.</p> * <p>Looks like this:</p> * <pre> * <sdk> * <path>/opt/android-sdk-linux</path> * <platform>2.1</platform> * </sdk> * </pre> * <p>The <code><platform></code> parameter is optional, and corresponds to the * <code>platforms/android-*</code> directories in the Android SDK directory. Default is the latest available * version, so you only need to set it if you for example want to use platform 1.5 but also have e.g. 2.2 installed. * Has no effect when used on an Android SDK 1.1. The parameter can also be coded as the API level. Therefore valid values are * 1.1, 1.5, 1.6, 2.0, 2.01, 2.1, 2,2 as well as 3, 4, 5, 6, 7, 8. If a platform/api level is not installed on the * machine an error message will be produced. </p> * <p>The <code><path></code> parameter is optional. The default is the setting of the ANDROID_HOME environment * variable. The parameter can be used to override this setting with a different environment variable like this:</p> * <pre> * <sdk> * <path>${env.ANDROID_SDK}</path> * </sdk> * </pre> * <p>or just with a hardcoded absolute path. The parameters can also be configured from command-line with parameters * <code>-Dandroid.sdk.path</code> and <code>-Dandroid.sdk.platform</code>.</p> * * @parameter */ private Sdk sdk; /** * <p>Parameter designed to pick up <code>-Dandroid.sdk.path</code> in case there is no pom with an * <code><sdk></code> configuration tag.</p> * <p>Corresponds to {@link Sdk#path}.</p> * * @parameter expression="${android.sdk.path}" * @readonly */ private File sdkPath; /** * <p>Parameter designed to pick up environment variable <code>ANDROID_HOME</code> in case * <code>android.sdk.path</code> is not configured.</p> * * @parameter expression="${env.ANDROID_HOME}" * @readonly */ private String envANDROID_HOME; /** * The <code>ANDROID_HOME</code> environment variable name. */ public static final String ENV_ANDROID_HOME = "ANDROID_HOME"; /** * <p>Parameter designed to pick up <code>-Dandroid.sdk.platform</code> in case there is no pom with an * <code><sdk></code> configuration tag.</p> * <p>Corresponds to {@link Sdk#platform}.</p> * * @parameter expression="${android.sdk.platform}" * @readonly */ private String sdkPlatform; /** * <p>Whether to undeploy an apk from the device before deploying it.</p> * <p/> * <p>Only has effect when running <code>mvn android:deploy</code> in an Android application project manually, or * when running <code>mvn integration-test</code> (or <code>mvn install</code>) in a project with instrumentation * tests. * </p> * <p/> * <p>It is useful to keep this set to <code>true</code> at all times, because if an apk with the same package was * previously signed with a different keystore, and deployed to the device, deployment will fail becuase your * keystore is different.</p> * * @parameter default-value=false * expression="${android.undeployBeforeDeploy}" */ protected boolean undeployBeforeDeploy; /** * <p>Whether to attach the normal .jar file to the build, so it can be depended on by for example integration-tests * which may then access {@code R.java} from this project.</p> * <p>Only disable it if you know you won't need it for any integration-tests. Otherwise, leave it enabled.</p> * * @parameter default-value=true * expression="${android.attachJar}" */ protected boolean attachJar; /** * <p>Whether to attach sources to the build, which can be depended on by other {@code apk} projects, for including * them in their builds.</p> * <p>Enabling this setting is only required if this project's source code and/or res(ources) will be included in * other projects, using the Maven <dependency> tag.</p> * * @parameter default-value=false * expression="${android.attachSources}" */ protected boolean attachSources; /** * Accessor for the local repository. * * @return The local repository. */ protected ArtifactRepository getLocalRepository() { return localRepository; } /** * Which dependency scopes should not be included when unpacking dependencies into the apk. */ protected static final List<String> EXCLUDED_DEPENDENCY_SCOPES = Arrays.asList("provided", "system", "import"); /** * @return a {@code Set} of dependencies which may be extracted and otherwise included in other artifacts. Never * {@code null}. This excludes artifacts of the {@code EXCLUDED_DEPENDENCY_SCOPES} scopes. */ protected Set<Artifact> getRelevantCompileArtifacts() { final List<Artifact> allArtifacts = (List<Artifact>) project.getCompileArtifacts(); final Set<Artifact> results = filterOutIrrelevantArtifacts(allArtifacts); return results; } /** * @return a {@code Set} of direct project dependencies. Never {@code null}. This excludes artifacts of the {@code * EXCLUDED_DEPENDENCY_SCOPES} scopes. */ protected Set<Artifact> getRelevantDependencyArtifacts() { final Set<Artifact> allArtifacts = (Set<Artifact>) project.getDependencyArtifacts(); final Set<Artifact> results = filterOutIrrelevantArtifacts(allArtifacts); return results; } private Set<Artifact> filterOutIrrelevantArtifacts(Iterable<Artifact> allArtifacts) { final Set<Artifact> results = new LinkedHashSet<Artifact>(); for (Artifact artifact : allArtifacts) { if (artifact == null) { continue; } if (EXCLUDED_DEPENDENCY_SCOPES.contains(artifact.getScope())) { continue; } // TODO: this statement must be retired in version 3.0, but we can't do that yet because we promised to not break backwards compatibility within the 2.x series. if (artifact.getGroupId().equals("android")) { getLog().warn("Excluding the android.jar from being unpacked into your apk file, based on its <groupId>android</groupId>. Please set <scope>provided</scope> in that dependency, because that is the correct way, and the only which will work in the future."); continue; } results.add(artifact); } return results; } /** * Attempts to resolve an {@link Artifact} to a {@link File}. * * @param artifact to resolve * @return a {@link File} to the resolved artifact, never <code>null</code>. * @throws MojoExecutionException if the artifact could not be resolved. */ protected File resolveArtifactToFile(Artifact artifact) throws MojoExecutionException { final ArtifactsResolver artifactsResolver = new DefaultArtifactsResolver(this.artifactResolver, this.localRepository, this.remoteRepositories, true); final HashSet<Artifact> artifacts = new HashSet<Artifact>(); artifacts.add(artifact); File jar = null; final Set<Artifact> resolvedArtifacts = artifactsResolver.resolve(artifacts, getLog()); for (Artifact resolvedArtifact : resolvedArtifacts) { jar = resolvedArtifact.getFile(); } if (jar == null) { throw new MojoExecutionException("Could not resolve artifact " + artifact.getId() + ". Please install it with \"mvn install:install-file ...\" or deploy it to a repository with \"mvn deploy:deploy-file ...\""); } return jar; } /** * Deploys an apk file to a connected emulator or usb device. * * @param apkFile the file to deploy * @throws MojoExecutionException If there is a problem deploying the apk file. */ protected void deployApk(File apkFile) throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger(this.getLog()); List<String> commands = new ArrayList<String>(); addDeviceParameter(commands); commands.add("install"); commands.add("-r"); commands.add(apkFile.getAbsolutePath()); getLog().info(getAndroidSdk().getPathForTool("adb") + " " + commands.toString()); try { executor.executeCommand(getAndroidSdk().getPathForTool("adb"), commands, false); final String standardOut = executor.getStandardOut(); if (standardOut != null && standardOut.contains("Failure")) { throw new MojoExecutionException("Error deploying " + apkFile + " to device. You might want to add command line parameter -Dandroid.undeployBeforeDeploy=true or add plugin configuration tag <undeployBeforeDeploy>true</undeployBeforeDeploy>\n" + standardOut); } } catch (ExecutionException e) { getLog().error(executor.getStandardOut()); getLog().error(executor.getStandardError()); throw new MojoExecutionException("Error deploying " + apkFile + " to device.", e); } } /** * Checks if a specific device should be used, and adds any relevant parameter(s) to the parameters list. * * @param commands the parameters to be used with the {@code adb} command */ protected void addDeviceParameter(List<String> commands) { if (StringUtils.isNotBlank(device)) { if ("usb".equals(device)) { commands.add("-d"); } else if ("emulator".equals(device)) { commands.add("-e"); } else { commands.add("-s"); commands.add(device); } } } /** * Undeploys an apk from a connected emulator or usb device. Also deletes the application's data and cache * directories on the device. * * @param apkFile the file to undeploy * @return <code>true</code> if successfully uninstalled, <code>false</code> otherwise. */ protected boolean undeployApk(File apkFile) throws MojoExecutionException { return undeployApk(apkFile, true); } /** * Undeploys an apk from a connected emulator or usb device. Also deletes the application's data and cache * directories on the device. * * @param apkFile the file to undeploy * @param deleteDataAndCacheDirectoriesOnDevice * <code>true</code> to delete the application's data and cache * directories on the device, <code>false</code> to keep them. * @return <code>true</code> if successfully undeployed, <code>false</code> otherwise. */ protected boolean undeployApk(File apkFile, boolean deleteDataAndCacheDirectoriesOnDevice) throws MojoExecutionException { final String packageName; packageName = extractPackageNameFromApk(apkFile); return undeployApk(packageName, deleteDataAndCacheDirectoriesOnDevice); } /** * Undeploys an apk, specified by package name, from a connected emulator or usb device. Also deletes the * application's data and cache directories on the device. * * @param packageName the package name to undeploy. * @return <code>true</code> if successfully undeployed, <code>false</code> otherwise. */ protected boolean undeployApk(String packageName) throws MojoExecutionException { return undeployApk(packageName, true); } /** * Undeploys an apk, specified by package name, from a connected emulator or usb device. * * @param packageName the package name to undeploy. * @param deleteDataAndCacheDirectoriesOnDevice * <code>true</code> to delete the application's data and cache * directories on the device, <code>false</code> to keep them. * @return <code>true</code> if successfully undeployed, <code>false</code> otherwise. */ protected boolean undeployApk(String packageName, boolean deleteDataAndCacheDirectoriesOnDevice) throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger(this.getLog()); List<String> commands = new ArrayList<String>(); addDeviceParameter(commands); commands.add("uninstall"); if (!deleteDataAndCacheDirectoriesOnDevice) { commands.add("-k"); // ('-k' means keep the data and cache directories) } commands.add(packageName); getLog().info(getAndroidSdk().getAdbPath() + " " + commands.toString()); try { executor.executeCommand(getAndroidSdk().getAdbPath(), commands, false); getLog().debug(executor.getStandardOut()); getLog().debug(executor.getStandardError()); return true; } catch (ExecutionException e) { getLog().error(executor.getStandardOut()); getLog().error(executor.getStandardError()); return false; } } /** * Extracts the package name from an apk file. * * @param apkFile apk file to extract package name from. * @return the package name from inside the apk file. */ protected String extractPackageNameFromApk(File apkFile) throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger(this.getLog()); List<String> commands = new ArrayList<String>(); commands.add("dump"); commands.add("xmltree"); commands.add(apkFile.getAbsolutePath()); commands.add("AndroidManifest.xml"); getLog().info(getAndroidSdk().getPathForTool("aapt") + " " + commands.toString()); try { executor.executeCommand(getAndroidSdk().getPathForTool("aapt"), commands, true); final String xmlTree = executor.getStandardOut(); return extractPackageNameFromAndroidManifestXmlTree(xmlTree); } catch (ExecutionException e) { throw new MojoExecutionException("Error while trying to figure out package name from inside apk file " + apkFile); } finally { getLog().error(executor.getStandardError()); } } /** * Extracts the package name from an XmlTree dump of AndroidManifest.xml by the <code>aapt</code> tool. * * @param aaptDumpXmlTree output from <code>aapt dump xmltree <apkFile> AndroidManifest.xml * @return the package name from inside the apkFile. */ protected String extractPackageNameFromAndroidManifestXmlTree(String aaptDumpXmlTree) { final Scanner scanner = new Scanner(aaptDumpXmlTree); // Finds the root element named "manifest". scanner.findWithinHorizon("^E: manifest", 0); // Finds the manifest element's attribute named "package". scanner.findWithinHorizon(" A: package=\"", 0); // Extracts the package value including the trailing double quote. String packageName = scanner.next(".*?\""); // Removes the double quote. packageName = packageName.substring(0, packageName.length() - 1); return packageName; } protected String extractPackageNameFromAndroidManifest(File androidManifestFile) throws MojoExecutionException { final URL xmlURL; try { xmlURL = androidManifestFile.toURI().toURL(); } catch (MalformedURLException e) { throw new MojoExecutionException("Error while trying to figure out package name from inside AndroidManifest.xml file " + androidManifestFile, e); } final DocumentContainer documentContainer = new DocumentContainer(xmlURL); final Object packageName = JXPathContext.newContext(documentContainer).getValue("manifest/@package", String.class); return (String) packageName; } /** * Attempts to find the instrumentation test runner from inside the AndroidManifest.xml file. * * @param androidManifestFile the AndroidManifest.xml file to inspect. * @return the instrumentation test runner declared in AndroidManifest.xml, or {@code null} if it is not declared. * @throws MojoExecutionException */ protected String extractInstrumentationRunnerFromAndroidManifest(File androidManifestFile) throws MojoExecutionException { final URL xmlURL; try { xmlURL = androidManifestFile.toURI().toURL(); } catch (MalformedURLException e) { throw new MojoExecutionException("Error while trying to figure out instrumentation runner from inside AndroidManifest.xml file " + androidManifestFile, e); } final DocumentContainer documentContainer = new DocumentContainer(xmlURL); final Object instrumentationRunner; try { instrumentationRunner = JXPathContext.newContext(documentContainer).getValue("manifest//instrumentation/@android:name", String.class); } catch (JXPathNotFoundException e) { return null; } return (String) instrumentationRunner; } protected int deleteFilesFromDirectory(File baseDirectory, String... includes) throws MojoExecutionException { final String[] files = findFilesInDirectory(baseDirectory, includes); if (files == null) { return 0; } for (String file : files) { final boolean successfullyDeleted = new File(baseDirectory, file).delete(); if (!successfullyDeleted) { throw new MojoExecutionException("Failed to delete \"" + file + "\""); } } return files.length; } /** * Finds files. * * @param baseDirectory Directory to find files in. * @param includes Ant-style include statements, for example <code>"** /*.aidl"</code> (but without the space in the middle) * @return <code>String[]</code> of the files' paths and names, relative to <code>baseDirectory</code>. Empty <code>String[]</code> if <code>baseDirectory</code> does not exist. */ protected String[] findFilesInDirectory(File baseDirectory, String... includes) { if (baseDirectory.exists()) { DirectoryScanner directoryScanner = new DirectoryScanner(); directoryScanner.setBasedir(baseDirectory); directoryScanner.setIncludes(includes); directoryScanner.addDefaultExcludes(); directoryScanner.scan(); String[] files = directoryScanner.getIncludedFiles(); return files; } else { return new String[0]; } } /** * <p>Returns the Android SDK to use.</p> * <p/> * <p>Current implementation looks for <code><sdk><path></code> configuration in pom, then System * property <code>android.sdk.path</code>, then environment variable <code>ANDROID_HOME</code>. * <p/> * <p>This is where we collect all logic for how to lookup where it is, and which one to choose. The lookup is * based on available parameters. This method should be the only one you should need to look at to understand how * the Android SDK is chosen, and from where on disk.</p> * * @return the Android SDK to use. * @throws org.apache.maven.plugin.MojoExecutionException * if no Android SDK path configuration is available at all. */ protected AndroidSdk getAndroidSdk() throws MojoExecutionException { File chosenSdkPath; String chosenSdkPlatform; if (sdk != null) { // An <sdk> tag exists in the pom. if (sdk.getPath() != null) { // An <sdk><path> tag is set in the pom. chosenSdkPath = sdk.getPath(); } else { // There is no <sdk><path> tag in the pom. if (sdkPath != null) { // -Dandroid.sdk.path is set on command line, or via <properties><sdk.path>... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via <properties><sdk.path>... chosenSdkPath = new File(getAndroidHomeOrThrow()); } } // Use <sdk><platform> from pom if it's there, otherwise try -Dandroid.sdk.platform from command line or <properties><sdk.platform>... if (!isBlank(sdk.getPlatform())) { chosenSdkPlatform = sdk.getPlatform(); } else { chosenSdkPlatform = sdkPlatform; } } else { // There is no <sdk> tag in the pom. if (sdkPath != null) { // -Dandroid.sdk.path is set on command line, or via <properties><sdk.path>... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via <properties><sdk.path>... chosenSdkPath = new File(getAndroidHomeOrThrow()); } // Use any -Dandroid.sdk.platform from command line or <properties><sdk.platform>... chosenSdkPlatform = sdkPlatform; } return new AndroidSdk(chosenSdkPath, chosenSdkPlatform); } private String getAndroidHomeOrThrow() throws MojoExecutionException { final String androidHome = System.getenv(ENV_ANDROID_HOME); if (isBlank(androidHome)) { throw new MojoExecutionException("No Android SDK path could be found. You may configure it in the pom using <sdk><path>...</path></sdk> or <properties><sdk.path>...</sdk.path></properties> or on command-line using -Dandroid.sdk.path=... or by setting environment variable " + ENV_ANDROID_HOME); } return androidHome; } }