All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jayway.maven.plugins.android.AbstractAndroidMojo Maven / Gradle / Ivy

There is a newer version: 3.0.0-alpha-11
Show newest version
/*
 * 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 [email protected]
 */
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 AndroidManifest.xml 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;

    /**
     * The combined assets directory. This will contain both the assets found in "assets" as well as any assets contained in a apksources dependency.
     *
     * @parameter expression="${project.build.directory}/generated-sources/combined-assets/assets"
     * @readonly
     */
    protected File combinedAssets;

    /**
     * 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 hdpi will exclude all resource folders with the mdpi or ldpi
     * 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;

    /**
     * 

The Android SDK to use.

*

Looks like this:

*
     * <sdk>
     *     <path>/opt/android-sdk-linux</path>
     *     <platform>2.1</platform>
     * </sdk>
     * 
*

The <platform> parameter is optional, and corresponds to the * platforms/android-* 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.

*

The <path> 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:

*
     * <sdk>
     *     <path>${env.ANDROID_SDK}</path>
     * </sdk>
     * 
*

or just with a hardcoded absolute path. The parameters can also be configured from command-line with parameters * -Dandroid.sdk.path and -Dandroid.sdk.platform.

* * @parameter */ private Sdk sdk; /** *

Parameter designed to pick up -Dandroid.sdk.path in case there is no pom with an * <sdk> configuration tag.

*

Corresponds to {@link Sdk#path}.

* * @parameter expression="${android.sdk.path}" * @readonly */ private File sdkPath; /** *

Parameter designed to pick up environment variable ANDROID_HOME in case * android.sdk.path is not configured.

* * @parameter expression="${env.ANDROID_HOME}" * @readonly */ private String envANDROID_HOME; /** * The ANDROID_HOME environment variable name. */ public static final String ENV_ANDROID_HOME = "ANDROID_HOME"; /** *

Parameter designed to pick up -Dandroid.sdk.platform in case there is no pom with an * <sdk> configuration tag.

*

Corresponds to {@link Sdk#platform}.

* * @parameter expression="${android.sdk.platform}" * @readonly */ private String sdkPlatform; /** *

Whether to undeploy an apk from the device before deploying it.

*

*

Only has effect when running mvn android:deploy in an Android application project manually, or * when running mvn integration-test (or mvn install) in a project with instrumentation * tests. *

*

*

It is useful to keep this set to true 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.

* * @parameter default-value=false * expression="${android.undeployBeforeDeploy}" */ protected boolean undeployBeforeDeploy; /** *

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.

*

Only disable it if you know you won't need it for any integration-tests. Otherwise, leave it enabled.

* * @parameter default-value=true * expression="${android.attachJar}" */ protected boolean attachJar; /** *

Whether to attach sources to the build, which can be depended on by other {@code apk} projects, for including * them in their builds.

*

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.

* * @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 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 getRelevantCompileArtifacts() { final List allArtifacts = (List) project.getCompileArtifacts(); final Set 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 getRelevantDependencyArtifacts() { final Set allArtifacts = (Set) project.getDependencyArtifacts(); final Set results = filterOutIrrelevantArtifacts(allArtifacts); return results; } private Set filterOutIrrelevantArtifacts(Iterable allArtifacts) { final Set results = new LinkedHashSet(); 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 android. Please set provided 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 null. * @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 artifacts = new HashSet(); artifacts.add(artifact); File jar = null; final Set 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 commands = new ArrayList(); addDeviceParameter(commands); commands.add("install"); commands.add("-r"); commands.add(apkFile.getAbsolutePath()); getLog().info(getAndroidSdk().getAdbPath() + " " + commands.toString()); try { executor.executeCommand(getAndroidSdk().getAdbPath(), 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 true\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 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 true if successfully uninstalled, false 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 * true to delete the application's data and cache * directories on the device, false to keep them. * @return true if successfully undeployed, false 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 true if successfully undeployed, false 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 * true to delete the application's data and cache * directories on the device, false to keep them. * @return true if successfully undeployed, false otherwise. */ protected boolean undeployApk(String packageName, boolean deleteDataAndCacheDirectoriesOnDevice) throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger(this.getLog()); List commands = new ArrayList(); 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 commands = new ArrayList(); 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 aapt tool. * * @param aaptDumpXmlTree output from 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 "** /*.aidl" (but without the space in the middle) * @return String[] of the files' paths and names, relative to baseDirectory. Empty String[] if baseDirectory 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]; } } /** *

Returns the Android SDK to use.

*

*

Current implementation looks for <sdk><path> configuration in pom, then System * property android.sdk.path, then environment variable ANDROID_HOME. *

*

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.

* * @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 tag exists in the pom. if (sdk.getPath() != null) { // An tag is set in the pom. chosenSdkPath = sdk.getPath(); } else { // There is no tag in the pom. if (sdkPath != null) { // -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = new File(getAndroidHomeOrThrow()); } } // Use from pom if it's there, otherwise try -Dandroid.sdk.platform from command line or ... if (!isBlank(sdk.getPlatform())) { chosenSdkPlatform = sdk.getPlatform(); } else { chosenSdkPlatform = sdkPlatform; } } else { // There is no tag in the pom. if (sdkPath != null) { // -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = new File(getAndroidHomeOrThrow()); } // Use any -Dandroid.sdk.platform from command line or ... 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 ... or ... or on command-line using -Dandroid.sdk.path=... or by setting environment variable " + ENV_ANDROID_HOME); } return androidHome; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy