/* * ### * Android Maven Plugin - android-maven-plugin * * Copyright (C) 1999 - 2012 Photon Infotech Inc. * * 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. * ### */ /* * Copyright (C) 2009 Jayway AB * * 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.photon.maven.plugins.android; import static com.photon.maven.plugins.android.common.AndroidExtension.APK; import static org.apache.commons.lang.StringUtils.isBlank; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Scanner; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; 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.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.codehaus.plexus.util.DirectoryScanner; import org.sonatype.aether.RepositorySystem; import org.sonatype.aether.RepositorySystemSession; import org.sonatype.aether.repository.RemoteRepository; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.IDevice; import com.android.ddmlib.InstallException; import com.photon.maven.plugins.android.common.AetherHelper; import com.photon.maven.plugins.android.common.AndroidExtension; import com.photon.maven.plugins.android.common.DeviceHelper; import com.photon.maven.plugins.android.configuration.Sdk; /** * Contains common fields and methods for android mojos. * */ public abstract class AbstractAndroidMojo extends AbstractMojo { public static final List<String> SUPPORTED_PACKAGING_TYPES = new ArrayList<String>(); static { SUPPORTED_PACKAGING_TYPES.add(AndroidExtension.APK); SUPPORTED_PACKAGING_TYPES.add(AndroidExtension.APKPERF); } /** Android Debug Bridge initialisation timeout in milliseconds. */ private static final long ADB_TIMEOUT_MS = 60L * 1000; /** * 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}/source/res" */ protected File resourceDirectory; /** * <p> * Root folder containing native libraries to include in the application * package. * </p> * * @parameter expression="${android.nativeLibrariesDirectory}" * default-value="${project.basedir}/libs" */ protected File nativeLibrariesDirectory; /** * 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}/source/assets" */ protected File assetsDirectory; /** * The <code>AndroidManifest.xml</code> file. * * @parameter default-value="${project.basedir}/source/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; /** * Extract the apklib dependencies here * * @parameter expression="${project.build.directory}/unpack/apklibs" * @readonly */ protected File unpackedApkLibsDirectory; /** * 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; /** * A list of extra arguments that must be passed to aapt. * * @parameter expression="${android.aaptExtraArgs}" */ protected String[] aaptExtraArgs; /** * 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; /** * The entry point to Aether, i.e. the component doing all the work. * * @component */ protected RepositorySystem repoSystem; /** * The current repository/network configuration of Maven. * * @parameter default-value="${repositorySystemSession}" * @readonly */ protected RepositorySystemSession repoSession; /** * The project's remote repositories to use for the resolution of project * dependencies. * * @parameter default-value="${project.remoteProjectRepositories}" * @readonly */ protected List<RemoteRepository> projectRepos; /** * Generates R.java into a different package. * * @parameter expression="${android.customPackage}" */ protected String customPackage; /** * 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 com.photon.maven.plugins.android.configuration.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 com.photon.maven.plugins.android.configuration.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; /** * @parameter expression="${project.basedir}" required="true" * @readonly */ protected File baseDir; /** * @parameter expression="${environmentName}" required="true" */ protected String environmentName; private static final Object ADBLOCK = new Object(); private static boolean adbInitialized = false; private String[] deviceSerialNumberArr = null; /** * 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"); */ protected static final List<String> EXCLUDED_DEPENDENCY_SCOPES = Arrays .asList("provided", "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 = 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 = project.getDependencyArtifacts(); final Set<Artifact> results = filterOutIrrelevantArtifacts(allArtifacts); return results; } /** * @return a {@code List} of all project dependencies. Never {@code null}. * This excludes artifacts of the {@code EXCLUDED_DEPENDENCY_SCOPES} * scopes. And This should maintain dependency order to comply with * library project resource precedence. */ protected Set<Artifact> getAllRelevantDependencyArtifacts() { final Set<Artifact> allArtifacts = project.getArtifacts(); 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; } if ("apk".equalsIgnoreCase(artifact.getType())) { 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 { Artifact resolvedArtifact = AetherHelper.resolveArtifact(artifact, repoSystem, repoSession, projectRepos); final File 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; } /** * 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. * * @return */ protected AndroidDebugBridge initAndroidDebugBridge() throws MojoExecutionException { synchronized (ADBLOCK) { if (!adbInitialized) { 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. * * @param adb */ private void waitUntilConnected(AndroidDebugBridge adb) { int trials = 10; while (trials > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } if (adb.isConnected()) { break; } trials--; } } /** * Wait for the Android Debug Bridge to return an initial device list. * * @param androidDebugBridge * @throws MojoExecutionException */ 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() { @Override public void doWithDevice(final IDevice device) throws MojoExecutionException { try { device.installPackage(apkFile.getAbsolutePath(), true); getLog().info( "Successfully installed " + apkFile.getAbsolutePath() + " to " + DeviceHelper.getDescriptiveName(device)); } catch (InstallException e) { throw new MojoExecutionException("Install of " + apkFile.getAbsolutePath() + "failed.", e); } } }); } protected void deployDependencies() throws MojoExecutionException, MojoFailureException { Set<Artifact> 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); /* * if (undeployBeforeDeploy) { getLog().debug( * "Attempting undeploy of " + targetApkFile + * " from device..."); undeployApk(targetApkFile); } */ getLog().debug( "Deploying " + targetApkFile + " to device..."); deployApk(targetApkFile); } } } } protected void deployBuiltApk() throws MojoExecutionException, MojoFailureException { // If we're not on a supported packaging with just skip (Issue 112) // http://code.google.com/p/maven-android-plugin/issues/detail?id=112 if (!SUPPORTED_PACKAGING_TYPES.contains(project.getPackaging())) { getLog().info("Skipping deployment on " + project.getPackaging()); return; } File apkFile = new File(project.getBuild().getDirectory(), project .getBuild().getFinalName() + "." + APK); deployApk(apkFile); } /** * Determines which {@link IDevice}(s) to use, and performs the callback * action on it/them. * * @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()) { waitForInitialDeviceList(androidDebugBridge); List<IDevice> devices = Arrays.asList(androidDebugBridge .getDevices()); int numberOfDevices = devices.size(); getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge"); if (devices.size() > 0) { if (StringUtils.isNotBlank(device)) { boolean deviceFound = false; if ("emulator".equals(device) || ("usb".equals(device))) { getLog().info( "android.device parameter set to " + device); for (IDevice idevice : devices) { // use specified device or all emulators or all // devices if ("emulator".equals(device) && idevice.isEmulator()) { getLog().info( "Emulator " + DeviceHelper .getDescriptiveName(idevice) + " found."); deviceFound = true; deviceCallback.doWithDevice(idevice); } else if ("usb".equals(device) && !idevice.isEmulator()) { getLog().info( "Device " + DeviceHelper .getDescriptiveName(idevice) + " found."); deviceFound = true; deviceCallback.doWithDevice(idevice); } } } else { getLog().info( "android.device parameter set to serial number: " + device); if (StringUtils.indexOf(device, ",") > -1) { deviceSerialNumberArr = device.trim().split(","); for (final IDevice idevice : devices) { for (int i = 0; i < deviceSerialNumberArr.length; i++) { if (idevice.isEmulator() && (deviceSerialNumberArr[i] .equalsIgnoreCase(idevice .getAvdName()) || deviceSerialNumberArr[i] .equalsIgnoreCase(idevice .getSerialNumber()))) { getLog().info( "Emulator " + DeviceHelper .getDescriptiveName(idevice) + " found."); deviceFound = true; deviceCallback.doWithDevice(idevice); break; } else if (!idevice.isEmulator() && deviceSerialNumberArr[i] .equals(idevice .getSerialNumber())) { getLog().info( "Device " + DeviceHelper .getDescriptiveName(idevice) + " found."); deviceFound = true; deviceCallback.doWithDevice(idevice); break; } } } } else { deviceSerialNumberArr = new String[1]; deviceSerialNumberArr[0] = device.trim(); for (final IDevice idevice : devices) { for (int i = 0; i < deviceSerialNumberArr.length; i++) { if (idevice.isEmulator() && (deviceSerialNumberArr[i] .equalsIgnoreCase(idevice .getAvdName()) || deviceSerialNumberArr[i] .equalsIgnoreCase(idevice .getSerialNumber()))) { getLog().info( "Emulator " + DeviceHelper .getDescriptiveName(idevice) + " found."); deviceFound = true; deviceCallback.doWithDevice(idevice); break; } else if (!idevice.isEmulator() && deviceSerialNumberArr[i] .equals(idevice .getSerialNumber())) { getLog().info( "Device " + DeviceHelper .getDescriptiveName(idevice) + " found."); deviceFound = true; deviceCallback.doWithDevice(idevice); break; } } } } } if (!deviceFound) { throw new MojoExecutionException( "No device found for android.device=" + device); } } else { getLog().info( "android.device parameter not set, using all attached devices"); for (IDevice idevice : devices) { deviceCallback.doWithDevice(idevice); } } } else { throw new MojoExecutionException("No online devices attached."); } } else { throw new MojoExecutionException( "Android Debug Bridge is not connected."); } } /** * 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 undeployed, <code>false</code> * 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 <code>true</code> if successfully undeployed, <code>false</code> * 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() { @Override public void doWithDevice(final IDevice device) throws MojoExecutionException { try { device.uninstallPackage(packageName); getLog().info( "Successfully uninstalled " + packageName + " from " + DeviceHelper.getDescriptiveName(device)); result.set(true); } catch (InstallException e) { result.set(false); throw new MojoExecutionException("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()); 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, 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 <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><android.sdk.path>... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via // <properties><android.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><android.sdk.path>... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via // <properties><android.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 " + "plugin configuration section in the pom file using <sdk><path>...</path></sdk> or " + "<properties><android.sdk.path>...</android.sdk.path></properties> or on command-line " + "using -Dandroid.sdk.path=... or by setting environment variable " + ENV_ANDROID_HOME); } return androidHome; } protected String getLibraryUnpackDirectory(Artifact apkLibraryArtifact) { return AbstractAndroidMojo.getLibraryUnpackDirectory( unpackedApkLibsDirectory, apkLibraryArtifact); } public static String getLibraryUnpackDirectory( File unpackedApkLibsDirectory, Artifact apkLibraryArtifact) { return unpackedApkLibsDirectory.getAbsolutePath() + File.separator + apkLibraryArtifact.getId().replace(":", "_"); } }