com.simpligility.maven.plugins.android.AbstractAndroidMojo Maven / Gradle / Ivy
Show all versions of android-maven-plugin Show documentation
/*
* Copyright (C) 2009-2011 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.simpligility.maven.plugins.android;
import com.android.builder.core.VariantConfiguration;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.DdmPreferences;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.InstallException;
import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
import com.simpligility.maven.plugins.android.common.AndroidExtension;
import com.simpligility.maven.plugins.android.common.ArtifactResolverHelper;
import com.simpligility.maven.plugins.android.common.DependencyResolver;
import com.simpligility.maven.plugins.android.common.DeviceHelper;
import com.simpligility.maven.plugins.android.common.MavenToPlexusLogAdapter;
import com.simpligility.maven.plugins.android.common.NativeHelper;
import com.simpligility.maven.plugins.android.common.UnpackedLibHelper;
import com.simpligility.maven.plugins.android.config.ConfigPojo;
import com.simpligility.maven.plugins.android.configuration.Ndk;
import com.simpligility.maven.plugins.android.configuration.Sdk;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
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.handler.ArtifactHandler;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
import static org.apache.commons.lang.StringUtils.isBlank;
/**
* Contains common fields and methods for android mojos.
*
* @author [email protected]
* @author Manfred Moser
* @author William Ferguson
* @author Malachi de AElfweald [email protected]
* @author Roy Clarkson
*/
public abstract class AbstractAndroidMojo extends AbstractMojo
{
public static final List SUPPORTED_PACKAGING_TYPES = new ArrayList();
static
{
SUPPORTED_PACKAGING_TYPES.add( AndroidExtension.APK );
}
/**
* Android Debug Bridge initialization timeout in milliseconds.
*/
private static final long ADB_TIMEOUT_MS = 60L * 1000;
/**
* The ANDROID_NDK_HOME
environment variable name.
*/
public static final String ENV_ANDROID_NDK_HOME = "ANDROID_NDK_HOME";
/**
* The Android NDK to use.
* Looks like this:
*
* <ndk>
* <path>/opt/android-ndk-r4</path>
* </ndk>
*
* The <path>
parameter is optional. The default is the setting of the ANDROID_NDK_HOME
* environment variable. The parameter can be used to override this setting with a different environment variable
* like this:
*
* <ndk>
* <path>${env.ANDROID_NDK_HOME}</path>
* </ndk>
*
* or just with a hardcoded absolute path. The parameters can also be configured from command-line with parameter
* -Dandroid.ndk.path
.
*/
@Parameter
@ConfigPojo( prefix = "ndk" )
private Ndk ndk;
/**
* The maven project.
*/
@Component
protected MavenProject project;
/**
* The maven session.
*/
@Component
protected MavenSession session;
/**
*/
@Component
protected MojoExecution execution;
/**
* The java sources directory.
*/
@Parameter( defaultValue = "${project.build.sourceDirectory}", readonly = true )
protected File sourceDirectory;
/**
* The project build directory. Ie target.
*/
@Parameter( defaultValue = "${project.build.directory}", readonly = true )
protected File targetDirectory;
/**
* The output directory. Ie target/classes.
*/
@Parameter( defaultValue = "${project.build.outputDirectory}", readonly = true )
protected File projectOutputDirectory;
/**
* The project resources. By default a list containing src/main/resources.
*/
@Parameter( defaultValue = "${project.build.resources}", readonly = true )
protected List resources;
/**
* The final name of the artifact.
*/
@Parameter( defaultValue = "${project.build.finalName}", readonly = true )
protected String finalName;
/**
* The android resources directory.
*/
@Parameter( defaultValue = "${project.basedir}/src/main/res" )
protected File resourceDirectory;
/**
* The project source encoding. It will use the platform default encoding if the property is not set.
*/
@Parameter( defaultValue = "${project.build.sourceEncoding}", readonly = true )
protected String sourceEncoding;
/**
* Override default generated folder containing R.java
*/
@Parameter( property = "android.genDirectory", defaultValue = "${project.build.directory}/generated-sources/r" )
protected File genDirectory;
/**
* Root folder containing native libraries to include in the application package.
*/
@Parameter( property = "android.nativeLibrariesDirectory", defaultValue = "${project.basedir}/src/main/libs" )
protected File nativeLibrariesDirectory;
/**
* Folder in which the ndk libraries are collected ready for packaging.
*/
@Parameter( defaultValue = "${project.build.directory}/ndk-libs", readonly = true )
protected File ndkOutputDirectory;
/**
* The android resources overlay directory. This will be overridden
* by resourceOverlayDirectories if present.
*/
@Parameter( defaultValue = "${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( defaultValue = "${project.basedir}/src/main/assets" )
protected File assetsDirectory;
/**
* The AndroidManifest.xml
file.
*/
@Parameter( property = "android.manifestFile", defaultValue = "${project.basedir}/src/main/AndroidManifest.xml" )
protected File androidManifestFile;
/**
* Path to which to save the result of updating/merging/processing the source AndroidManifest.xml
* file ({@link androidManifestFile}).
*/
@Parameter( property = "destination.manifestFile", defaultValue = "${project.build.directory}/AndroidManifest.xml" )
protected File destinationManifestFile;
/**
* A possibly new package name for the application. This value will be passed on to the aapt
* parameter --rename-manifest-package. Look to aapt for more help on this.
*/
@Parameter( property = "android.renameManifestPackage" )
protected String renameManifestPackage;
@Parameter( defaultValue = "${project.build.directory}/generated-sources/extracted-dependencies", readonly = true )
protected File extractedDependenciesDirectory;
@Parameter(
defaultValue = "${project.build.directory}/generated-sources/extracted-dependencies/src/main/java",
readonly = true
)
protected File extractedDependenciesJavaSources;
@Parameter(
defaultValue = "${project.build.directory}/generated-sources/extracted-dependencies/src/main/resources",
readonly = true
)
protected File extractedDependenciesJavaResources;
/**
* The combined assets directory. This will contain both the assets found in "assets" as well as any assets
* contained in a apksources, apklib or aar dependencies.
*/
@Parameter( defaultValue = "${project.build.directory}/generated-sources/combined-assets", readonly = true )
protected File combinedAssets;
/**
* Include jars stored in the libs folder of an apklib as dependencies.
* Do not delete or change name as it is used in the LifeCycleParticipant.
*
* @see ClasspathModifierLifecycleParticipant
*/
@Parameter( defaultValue = "false" )
private boolean includeLibsJarsFromApklib;
/**
* Include jars stored in the libs folder of an aar as dependencies.
* Do not delete or change name as it is used in the LifeCycleParticipant.
*
* @see ClasspathModifierLifecycleParticipant
*/
@Parameter( defaultValue = "true" )
private boolean includeLibsJarsFromAar;
/**
* Specifies which the serial number of the device to connect to. Using the special values "usb" or
* "emulator" is also valid. "usb" will connect to all actual devices connected (via usb). "emulator" will
* connect to all emulators connected. Multiple devices will be iterated over in terms of goals to run. All
* device interaction goals support this so you can e.. deploy the apk to all attached emulators and devices.
* Goals supporting this are devices, deploy, undeploy, redeploy, pull, push and instrument.
*/
@Parameter( property = "android.device" )
protected String device;
/**
* Specifies a list of serial numbers of each device you want to connect to. Using the special values "usb" or
* "emulator" is also valid. "usb" will connect to all actual devices connected (via usb). "emulator" will
* connect to all emulators connected. Multiple devices will be iterated over in terms of goals to run. All
* device interaction goals support this so you can e.. deploy the apk to all attached emulators and devices.
* Goals supporting this are devices, deploy, undeploy, redeploy, pull, push and instrument.
*
* <devices>
* <device>usb</device>
* <device>emulator-5554</device>
* </devices>
*
* This parameter can also be configured from command-line with
* parameter -Dandroid.devices=usb,emulator
.
*/
@Parameter( property = "android.devices" )
protected String[] devices;
/**
* Specifies the number of threads to use for deploying and testing on attached devices.
*
*
This parameter can also be configured from command-line with
* parameter -Dandroid.deviceThreads=2
.
*/
@Parameter( property = "android.deviceThreads" )
protected int deviceThreads;
/**
* External IP addresses. The connect goal of the android maven plugin will execute an adb connect on
* each IP address. If you have external dervice, you should call this connect goal before any other goal :
* mvn clean android:connect install.
* The Maven plugin will automatically add all these IP addresses into the the devices parameter.
* If you want to disconnect the IP addresses after the build, you can call the disconnect goal :
* mvn clean android:connect install android:disconnect
*
*
* <ips>
* <ip>127.0.0.1:5556</ip>
* </ips>
*
*/
@Parameter( property = "android.ips" )
protected String[] ips;
/**
* 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( property = "android.configurations" )
protected String configurations;
/**
* A list of extra arguments that must be passed to aapt.
*/
@Parameter( property = "android.aaptExtraArgs" )
protected String[] aaptExtraArgs;
/**
* Activate verbose output for the aapt execution in Maven debug mode. Defaults to "false"
*/
@Parameter( property = "android.aaptVerbose" )
protected boolean aaptVerbose;
/**
* Automatically create a ProGuard configuration file that will guard Activity classes and the like that are
* defined in the AndroidManifest.xml. This files is then automatically used in the proguard mojo execution,
* if enabled.
*/
@Parameter( property = "android.proguardFile" )
protected File proguardFile;
/**
* 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( property = "android.generateApk", defaultValue = "true" )
protected boolean generateApk;
@Component
private ArtifactResolver artifactResolver;
@Component
private ArtifactHandler artifactHandler;
/**
* Generates R.java into a different package.
*/
@Parameter( property = "android.customPackage" )
protected String customPackage;
/**
* Maven ProjectHelper.
*/
@Component
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 and so as well as 3, 4, 5, 6, 7, 8... 19. 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 hard-coded 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 com.simpligility.maven.plugins.android.configuration.Sdk#path}.
*/
@Parameter( property = "android.sdk.path", readonly = true )
private File sdkPath;
/**
* Parameter designed to pick up environment variable ANDROID_HOME
in case
* android.sdk.path
is not configured.
*/
@Parameter( defaultValue = "${env.ANDROID_HOME}", readonly = true )
private String envAndroidHome;
/**
* 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 com.simpligility.maven.plugins.android.configuration.Sdk#platform}.
*/
@Parameter( property = "android.sdk.platform", readonly = true )
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 because your
* keystore is different.
*/
@Parameter( property = "android.undeployBeforeDeploy", defaultValue = "false" )
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( property = "android.attachJar", defaultValue = "true" )
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( property = "android.attachSources", defaultValue = "false" )
protected boolean attachSources;
/**
* Parameter designed to pick up -Dandroid.ndk.path
in case there is no pom with an
* <ndk>
configuration tag.
* Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Ndk#path}.
*/
@Parameter( property = "android.ndk.path", readonly = true )
private File ndkPath;
/**
* Whether to create a release build (default is false / debug build). This affect BuildConfig generation
* and apk generation at this stage, but should probably affect other aspects of the build.
*/
@Parameter( property = "android.release", defaultValue = "false" )
protected boolean release;
/**
* The timeout value for an adb connection in milliseconds.
*/
@Parameter( property = "android.adb.connectionTimeout", defaultValue = "5000" )
protected int adbConnectionTimeout;
/**
* Folder in which AAR library dependencies will be unpacked.
*/
@Parameter( property = "unpackedLibsFolder", defaultValue = "${project.build.directory}/unpacked-libs" )
private File unpackedLibsFolder;
/**
* Whether the plugin should show a warning if conflicting dependencies with the Android provided ones exist.
*
* @see ClasspathModifierLifecycleParticipant
*/
@Parameter( defaultValue = "false" )
private File disableConflictingDependenciesWarning;
private UnpackedLibHelper unpackedLibHelper;
private ArtifactResolverHelper artifactResolverHelper;
private NativeHelper nativeHelper;
/**
*
*/
private static final Object ADB_LOCK = new Object();
/**
*
*/
private static boolean adbInitialized = false;
/**
* Dependency graph builder component.
*/
@Component( hint = "default" )
protected DependencyGraphBuilder dependencyGraphBuilder;
protected final DependencyResolver getDependencyResolver()
{
return new DependencyResolver( new MavenToPlexusLogAdapter( getLog() ), dependencyGraphBuilder );
}
/**
* @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 = project.getCompileArtifacts();
return getArtifactResolverHelper().getFilteredArtifacts( allArtifacts );
}
/**
* @return a {@code Set} of direct project dependencies. Never {@code null}. This excludes artifacts of the {@code
* EXCLUDED_DEPENDENCY_SCOPES} scopes.
*/
protected Set getDirectDependencyArtifacts()
{
final Set allArtifacts = project.getDependencyArtifacts();
return getArtifactResolverHelper().getFilteredArtifacts( allArtifacts );
}
/**
* Provides transitive dependency artifacts having types defined by {@code types} argument
* or all types if {@code types} argument is empty
*
* @param types artifact types to be selected
* @return a {@code List} of all project dependencies. Never {@code null}.
* This excludes artifacts of the {@link ArtifactResolverHelper.EXCLUDE_NON_PACKAGED_SCOPES} scopes.
* This should maintain dependency order to comply with library project resource precedence.
*/
protected Set getTransitiveDependencyArtifacts( String... types )
{
return getArtifactResolverHelper().getFilteredArtifacts( project.getArtifacts(), types );
}
/**
* Provides transitive dependency artifacts only defined types based on {@code types} argument
* or all types if {@code types} argument is empty
*
* @param filteredScopes List of scopes to be removed (ie filtered out).
* @param types Zero or more artifact types to be selected.
* @return a {@code List} of all project dependencies. Never {@code null}.
* This should maintain dependency order to comply with library project resource precedence.
*/
protected Set getTransitiveDependencyArtifacts( List filteredScopes, String... types )
{
return getArtifactResolverHelper().getFilteredArtifacts( filteredScopes, project.getArtifacts(), types );
}
/**
* 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
{
return getArtifactResolverHelper().resolveArtifactToFile( artifact );
}
/**
* Initialize the Android Debug Bridge and wait for it to start. Does not reinitialize it if it has
* already been initialized (that would through and IllegalStateException...). Synchronized sine
* the init call in the library is also synchronized .. just in case.
*/
protected AndroidDebugBridge initAndroidDebugBridge() throws MojoExecutionException
{
synchronized ( ADB_LOCK )
{
if ( ! adbInitialized )
{
DdmPreferences.setTimeOut( adbConnectionTimeout );
AndroidDebugBridge.init( false );
adbInitialized = true;
}
AndroidDebugBridge androidDebugBridge = AndroidDebugBridge
.createBridge( getAndroidSdk().getAdbPath(), false );
waitUntilConnected( androidDebugBridge );
return androidDebugBridge;
}
}
/**
* Run a wait loop until adb is connected or trials run out. This method seems to work more reliably then using a
* listener.
*/
private void waitUntilConnected( AndroidDebugBridge adb )
{
int trials = 10;
final int connectionWaitTime = 50;
while ( trials > 0 )
{
try
{
Thread.sleep( connectionWaitTime );
}
catch ( InterruptedException e )
{
e.printStackTrace();
}
if ( adb.isConnected() )
{
break;
}
trials--;
}
}
/**
* Wait for the Android Debug Bridge to return an initial device list.
*/
protected void waitForInitialDeviceList( final AndroidDebugBridge androidDebugBridge ) throws MojoExecutionException
{
if ( ! androidDebugBridge.hasInitialDeviceList() )
{
getLog().info( "Waiting for initial device list from the Android Debug Bridge" );
long limitTime = System.currentTimeMillis() + ADB_TIMEOUT_MS;
while ( ! androidDebugBridge.hasInitialDeviceList() && ( System.currentTimeMillis() < limitTime ) )
{
try
{
Thread.sleep( 1000 );
}
catch ( InterruptedException e )
{
throw new MojoExecutionException(
"Interrupted waiting for initial device list from Android Debug Bridge" );
}
}
if ( ! androidDebugBridge.hasInitialDeviceList() )
{
getLog().error( "Did not receive initial device list from the Android Debug Bridge." );
}
}
}
/**
* 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( final File apkFile ) throws MojoExecutionException, MojoFailureException
{
if ( undeployBeforeDeploy )
{
undeployApk( apkFile );
}
doWithDevices( new DeviceCallback()
{
public void doWithDevice( final IDevice device ) throws MojoExecutionException
{
String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
try
{
device.installPackage( apkFile.getAbsolutePath(), true );
getLog().info( deviceLogLinePrefix + "Successfully installed " + apkFile.getAbsolutePath() );
getLog().debug( " to " + DeviceHelper.getDescriptiveName( device ) );
}
catch ( InstallException e )
{
throw new MojoExecutionException( deviceLogLinePrefix + "Install of " + apkFile.getAbsolutePath()
+ " failed.", e );
}
}
} );
}
/**
*
* @throws MojoExecutionException
* @throws MojoFailureException
*/
protected void deployDependencies() throws MojoExecutionException, MojoFailureException
{
Set directDependentArtifacts = project.getDependencyArtifacts();
if ( directDependentArtifacts != null )
{
for ( Artifact artifact : directDependentArtifacts )
{
String type = artifact.getType();
if ( type.equals( APK ) )
{
getLog().debug( "Detected apk dependency " + artifact + ". Will resolve and deploy to device..." );
final File targetApkFile = resolveArtifactToFile( artifact );
getLog().debug( "Deploying " + targetApkFile + " to device..." );
deployApk( targetApkFile );
}
}
}
}
/**
* Deploy the apk built with the current projects to all attached devices and emulators.
* Skips other projects in a multi-module build without terminating.
*
* @throws MojoExecutionException
* @throws MojoFailureException
*/
protected void deployBuiltApk() throws MojoExecutionException, MojoFailureException
{
if ( project.getPackaging().equals( APK ) )
{
File apkFile = new File( targetDirectory, finalName + "." + APK );
deployApk( apkFile );
}
else
{
getLog().info( "Project packaging is not apk, skipping deployment." );
}
}
/**
* Performs the callback action on the devices determined by
* {@link #shouldDoWithThisDevice(com.android.ddmlib.IDevice)}
*
* @param deviceCallback the action to perform on each device
* @throws org.apache.maven.plugin.MojoExecutionException
* in case there is a problem
* @throws org.apache.maven.plugin.MojoFailureException
* in case there is a problem
*/
protected void doWithDevices( final DeviceCallback deviceCallback )
throws MojoExecutionException, MojoFailureException
{
final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
if ( !androidDebugBridge.isConnected() )
{
throw new MojoExecutionException( "Android Debug Bridge is not connected." );
}
waitForInitialDeviceList( androidDebugBridge );
List devices = Arrays.asList( androidDebugBridge.getDevices() );
int numberOfDevices = devices.size();
getLog().debug( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );
if ( devices.size() == 0 )
{
throw new MojoExecutionException( "No online devices attached." );
}
int threadCount = getDeviceThreads();
if ( getDeviceThreads() == 0 )
{
getLog().info( "android.devicesThreads parameter not set, using a thread for each attached device" );
threadCount = numberOfDevices;
}
else
{
getLog().info( "android.devicesThreads parameter set to " + getDeviceThreads() );
}
boolean shouldRunOnAllDevices = getDevices().size() == 0;
if ( shouldRunOnAllDevices )
{
getLog().info( "android.devices parameter not set, using all attached devices" );
}
else
{
getLog().info( "android.devices parameter set to " + getDevices().toString() );
}
ArrayList doThreads = new ArrayList();
ExecutorService executor = Executors.newFixedThreadPool( threadCount );
for ( final IDevice idevice : devices )
{
if ( shouldRunOnAllDevices )
{
String deviceType = idevice.isEmulator() ? "Emulator " : "Device ";
getLog().info( deviceType + DeviceHelper.getDescriptiveName( idevice ) + " found." );
}
if ( shouldRunOnAllDevices || shouldDoWithThisDevice( idevice ) )
{
DoThread deviceDoThread = new DoThread() {
public void runDo() throws MojoFailureException, MojoExecutionException
{
deviceCallback.doWithDevice( idevice );
}
};
doThreads.add( deviceDoThread );
executor.execute( deviceDoThread );
}
}
executor.shutdown();
while ( ! executor.isTerminated() )
{
// waiting for threads finish
}
throwAnyDoThreadErrors( doThreads );
if ( ! shouldRunOnAllDevices && doThreads.isEmpty() )
{
throw new MojoExecutionException( "No device found for android.device=" + getDevices().toString() );
}
}
private void throwAnyDoThreadErrors( ArrayList doThreads ) throws MojoExecutionException,
MojoFailureException
{
for ( DoThread deviceDoThread : doThreads )
{
if ( deviceDoThread.failure != null )
{
throw deviceDoThread.failure;
}
if ( deviceDoThread.execution != null )
{
throw deviceDoThread.execution;
}
}
}
/**
* Determines if this {@link IDevice}(s) should be used
*
* @param idevice the device to check
* @return if the device should be used
* @throws org.apache.maven.plugin.MojoExecutionException
* in case there is a problem
* @throws org.apache.maven.plugin.MojoFailureException
* in case there is a problem
*/
private boolean shouldDoWithThisDevice( IDevice idevice ) throws MojoExecutionException, MojoFailureException
{
for ( String device : getDevices() )
{
// use specified device or all emulators or all devices
if ( "emulator".equals( device ) && idevice.isEmulator() )
{
return true;
}
if ( "usb".equals( device ) && ! idevice.isEmulator() )
{
return true;
}
if ( idevice.isEmulator() && ( device.equalsIgnoreCase( idevice.getAvdName() ) || device
.equalsIgnoreCase( idevice.getSerialNumber() ) ) )
{
return true;
}
if ( ! idevice.isEmulator() && device.equals( idevice.getSerialNumber() ) )
{
return true;
}
}
return false;
}
/**
* 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 undeployed, false
otherwise.
*/
protected boolean undeployApk( File apkFile ) throws MojoExecutionException, MojoFailureException
{
final String packageName;
packageName = extractPackageNameFromApk( apkFile );
return undeployApk( packageName );
}
/**
* 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( final String packageName ) throws MojoExecutionException, MojoFailureException
{
final AtomicBoolean result = new AtomicBoolean( true ); // if no devices are present, it counts as successful
doWithDevices( new DeviceCallback()
{
public void doWithDevice( final IDevice device ) throws MojoExecutionException
{
String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
try
{
device.uninstallPackage( packageName );
getLog().info( deviceLogLinePrefix + "Successfully uninstalled " + packageName );
getLog().debug( " from " + DeviceHelper.getDescriptiveName( device ) );
result.set( true );
}
catch ( InstallException e )
{
result.set( false );
throw new MojoExecutionException( deviceLogLinePrefix + "Uninstall of " + packageName
+ " failed.", e );
}
}
} );
return result.get();
}
/**
* 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() );
executor.setCaptureStdOut( true );
executor.setCaptureStdErr( true );
AaptCommandBuilder commandBuilder = AaptCommandBuilder
.dump( getLog() )
.xmlTree()
.setPathToApk( apkFile.getAbsolutePath() )
.addAssetFile( "AndroidManifest.xml" );
getLog().info( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
try
{
executor.executeCommand( getAndroidSdk().getAaptPath(), commandBuilder.build(), false );
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
{
String errout = executor.getStandardError();
if ( ( errout != null ) && ( errout.trim().length() > 0 ) )
{
getLog().error( errout );
}
}
}
/**
* 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;
}
/**
* Provides package name for android artifact.
*
* @param artifact android artifact which package have to be extracted
* @return package name
* @throws MojoExecutionException if there is no AndroidManifest.xml for provided artifact
* or appears error while parsing in {@link #extractPackageNameFromAndroidManifest(File)}
*
* @see #extractPackageNameFromAndroidManifest(File)
*/
protected String extractPackageNameFromAndroidArtifact( Artifact artifact ) throws MojoExecutionException
{
final File unpackedLibFolder = getUnpackedLibFolder( artifact );
final File manifest = new File( unpackedLibFolder, "AndroidManifest.xml" );
if ( !manifest.exists() )
{
throw new MojoExecutionException(
"AndroidManifest.xml file wasn't found in next place: " + unpackedLibFolder );
}
return extractPackageNameFromAndroidManifest( manifest );
}
protected String extractPackageNameFromAndroidManifest( File manifestFile )
{
return VariantConfiguration.getManifestPackage( manifestFile );
}
/**
* @return the package name from this project's Android Manifest.
*/
protected final String getAndroidManifestPackageName()
{
return extractPackageNameFromAndroidManifest( destinationManifestFile );
}
/**
* Attempts to find the instrumentation test runner from inside the AndroidManifest.xml file.
*
* @param manifestFile 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 manifestFile )
throws MojoExecutionException
{
final URL xmlURL;
try
{
xmlURL = manifestFile.toURI().toURL();
}
catch ( MalformedURLException e )
{
throw new MojoExecutionException(
"Error while trying to figure out instrumentation runner from inside AndroidManifest.xml file "
+ manifestFile, 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 final boolean isInstrumentationTest() throws MojoExecutionException
{
return extractInstrumentationRunnerFromAndroidManifest( destinationManifestFile ) != null;
}
/**
* Returns the Android SDK to use.
*
* Current implementation looks for System property android.sdk.path
, then
* <sdk><path>
configuration in pom, 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;
String buildToolsVersion = null;
if ( sdk != null )
{
// An tag exists in the pom.
buildToolsVersion = sdk.getBuildTools();
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, buildToolsVersion );
}
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 "
+ "plugin configuration section in the pom file using ... or "
+ "... or on command-line "
+ "using -Dandroid.sdk.path=... or by setting environment variable " + ENV_ANDROID_HOME );
}
return androidHome;
}
protected final File getUnpackedLibsDirectory()
{
return getUnpackedLibHelper().getUnpackedLibsFolder();
}
public final File getUnpackedLibFolder( Artifact artifact )
{
return getUnpackedLibHelper().getUnpackedLibFolder( artifact );
}
protected final File getUnpackedAarClassesJar( Artifact artifact )
{
return getUnpackedLibHelper().getUnpackedClassesJar( artifact );
}
protected final File getUnpackedApkLibSourceFolder( Artifact artifact )
{
return getUnpackedLibHelper().getUnpackedApkLibSourceFolder( artifact );
}
protected final File getUnpackedLibResourceFolder( Artifact artifact )
{
return getUnpackedLibHelper().getUnpackedLibResourceFolder( artifact );
}
protected final File getUnpackedLibAssetsFolder( Artifact artifact )
{
return getUnpackedLibHelper().getUnpackedLibAssetsFolder( artifact );
}
/**
* @param artifact Android dependency that is being referenced.
* @return Folder where the unpacked native libraries are located.
*/
public final File getUnpackedLibNativesFolder( Artifact artifact )
{
return getUnpackedLibHelper().getUnpackedLibNativesFolder( artifact );
}
// TODO Replace this with a non-static method (could even replace it with one of the methods above).
public static File getLibraryUnpackDirectory( File unpackedApkLibsDirectory, Artifact artifact )
{
return new File( unpackedApkLibsDirectory.getAbsolutePath(), artifact.getArtifactId() );
}
/**
* Returns the Android NDK to use.
*
* Current implementation looks for <ndk><path>
configuration in pom, then System
* property android.ndk.path
, then environment variable ANDROID_NDK_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 NDK is chosen, and from where on disk.
*
* @return the Android NDK to use.
* @throws org.apache.maven.plugin.MojoExecutionException
* if no Android NDK path configuration is available at all.
*/
protected AndroidNdk getAndroidNdk() throws MojoExecutionException
{
File chosenNdkPath;
// There is no tag in the pom.
if ( ndkPath != null )
{
// -Dandroid.ndk.path is set on command line, or via ...
chosenNdkPath = ndkPath;
}
else if ( ndk != null && ndk.getPath() != null )
{
chosenNdkPath = ndk.getPath();
}
else
{
// No -Dandroid.ndk.path is set on command line, or via ...
chosenNdkPath = new File( getAndroidNdkHomeOrThrow() );
}
return new AndroidNdk( chosenNdkPath );
}
private String getAndroidNdkHomeOrThrow() throws MojoExecutionException
{
final String androidHome = System.getenv( ENV_ANDROID_NDK_HOME );
if ( isBlank( androidHome ) )
{
throw new MojoExecutionException( "No Android NDK path could be found. You may configure it in the pom "
+ "using ... or ... or on "
+ "command-line using -Dandroid.ndk.path=... or by setting environment variable "
+ ENV_ANDROID_NDK_HOME );
}
return androidHome;
}
/**
* @return the resource directories if defined or the resource directory.
*/
public File[] getResourceOverlayDirectories()
{
File[] overlayDirectories;
if ( resourceOverlayDirectories == null || resourceOverlayDirectories.length == 0 )
{
overlayDirectories = new File[]{ resourceOverlayDirectory };
}
else
{
overlayDirectories = resourceOverlayDirectories;
}
return overlayDirectories;
}
private Set getDevices()
{
Set list = new HashSet();
if ( StringUtils.isNotBlank( device ) )
{
list.add( device );
}
list.addAll( Arrays.asList( devices ) );
list.addAll( Arrays.asList( ips ) );
return list;
}
private int getDeviceThreads()
{
return deviceThreads;
}
private abstract class DoThread extends Thread
{
private MojoFailureException failure;
private MojoExecutionException execution;
public final void run()
{
try
{
runDo();
}
catch ( MojoFailureException e )
{
failure = e;
}
catch ( MojoExecutionException e )
{
execution = e;
}
}
protected abstract void runDo() throws MojoFailureException, MojoExecutionException;
}
/**
* @return True if this project constructs an APK as opposed to an AAR or APKLIB.
*/
protected final boolean isAPKBuild()
{
return getUnpackedLibHelper().isAPKBuild( project );
}
/**
* Copies the files contained within the source folder to the target folder.
*
* The the target folder doesn't exist it will be created.
*
*
* @param sourceFolder Folder from which to copy the resources.
* @param targetFolder Folder to which to copy the files.
* @throws MojoExecutionException if the files cannot be copied.
*/
protected final void copyFolder( File sourceFolder, File targetFolder ) throws MojoExecutionException
{
copyFolder( sourceFolder, targetFolder, TrueFileFilter.TRUE );
}
private void copyFolder( File sourceFolder, File targetFolder, FileFilter filter ) throws MojoExecutionException
{
if ( !sourceFolder.exists() )
{
return;
}
try
{
getLog().debug( "Copying " + sourceFolder + " to " + targetFolder );
if ( ! targetFolder.exists() )
{
if ( ! targetFolder.mkdirs() )
{
throw new MojoExecutionException( "Could not create target directory " + targetFolder );
}
}
FileUtils.copyDirectory( sourceFolder, targetFolder, filter );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Could not copy source folder to target folder", e );
}
}
protected final UnpackedLibHelper getUnpackedLibHelper()
{
if ( unpackedLibHelper == null )
{
unpackedLibHelper = new UnpackedLibHelper(
getArtifactResolverHelper(),
project,
new MavenToPlexusLogAdapter( getLog() ),
unpackedLibsFolder
);
}
return unpackedLibHelper;
}
protected final ArtifactResolverHelper getArtifactResolverHelper()
{
if ( artifactResolverHelper == null )
{
artifactResolverHelper = new ArtifactResolverHelper(
artifactResolver,
new MavenToPlexusLogAdapter( getLog() ),
project.getRemoteArtifactRepositories()
);
}
return artifactResolverHelper;
}
protected final NativeHelper getNativeHelper()
{
if ( nativeHelper == null )
{
nativeHelper = new NativeHelper( project, dependencyGraphBuilder, getLog() );
}
return nativeHelper;
}
}