/* * ### * 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.phase09package; import static com.photon.maven.plugins.android.common.AndroidExtension.APK; import static com.photon.maven.plugins.android.common.AndroidExtension.APKLIB; import java.io.File; import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import org.apache.commons.lang.StringUtils; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.codehaus.plexus.util.AbstractScanner; import com.photon.maven.plugins.android.AbstractAndroidMojo; import com.photon.maven.plugins.android.AndroidSigner; import com.photon.maven.plugins.android.CommandExecutor; import com.photon.maven.plugins.android.ExecutionException; import com.photon.maven.plugins.android.common.NativeHelper; import com.photon.maven.plugins.android.configuration.Sign; import com.photon.maven.plugins.android.phase09package.ApkBuilder; import com.photon.phresco.commons.BuildInfo; import com.photon.phresco.plugin.commons.PluginUtils; import com.photon.phresco.exception.PhrescoException; /** * Creates the apk file. By default signs it with debug keystore.<br/> * Change that by setting configuration parameter * <code><sign><debug>false</debug></sign></code>. * * @goal apk * @phase package * @requiresDependencyResolution compile */ public class ApkMojo extends AbstractAndroidMojo { /** * <p> * How to sign the apk. * </p> * <p> * Looks like this: * </p> * * <pre> * <sign> * <debug>auto</debug> * </sign> * </pre> * <p> * Valid values for <code><debug></code> are: * <ul> * <li><code>true</code> = sign with the debug keystore. * <li><code>false</code> = don't sign with the debug keystore. * <li><code>both</code> = create a signed as well as an unsigned apk. * <li><code>auto</code> (default) = sign with debug keystore, unless * another keystore is defined. (Signing with other keystores is not yet * implemented. See <a * href="http://code.google.com/p/maven-android-plugin/issues/detail?id=2" * >Issue 2</a>.) * </ul> * </p> * <p> * Can also be configured from command-line with parameter * <code>-Dandroid.sign.debug</code>. * </p> * * @parameter */ private Sign sign; /** * <p> * Parameter designed to pick up <code>-Dandroid.sign.debug</code> in case * there is no pom with a <code><sign></code> configuration tag. * </p> * <p> * Corresponds to * {@link com.photon.maven.plugins.android.configuration.Sign#debug}. * </p> * * @parameter expression="${android.sign.debug}" default-value="auto" * @readonly */ private String signDebug; /** * <p> * 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. * </p> * * @parameter expression="${android.renameManifestPackage}" */ private String renameManifestPackage; /** * <p> * Rewrite the manifest so that all of its instrumentation components target * the given package. This value will be passed on to the aapt parameter * --rename-instrumentation-target-package. Look to aapt for more help on * this. * </p> * * @parameter expression="${android.renameInstrumentationTargetPackage}" */ private String renameInstrumentationTargetPackage; /** * <p> * Allows to detect and extract the duplicate files from embedded jars. In * that case, the plugin analyzes the content of all embedded dependencies * and checks they are no duplicates inside those dependencies. Indeed, * Android does not support duplicates, and all dependencies are inlined in * the APK. If duplicates files are found, the resource is kept in the first * dependency and removes from others. * * @parameter expression="${android.extractDuplicates}" * default-value="false" */ private boolean extractDuplicates; /** * <p> * Temporary folder for collecting native libraries. * </p> * * @parameter default-value="${project.build.directory}/libs" * @readonly */ private File nativeLibrariesOutputDirectory; /** * <p> * Default hardware architecture for native library dependencies (with * {@code <type>so</type>}). * </p> * <p> * This value is used for dependencies without classifier, if * {@code nativeLibrariesDependenciesHardwareArchitectureOverride} is not * set. * </p> * <p> * Valid values currently include {@code armeabi} and {@code armeabi-v7a}. * </p> * * @parameter expression= * "${android.nativeLibrariesDependenciesHardwareArchitectureDefault}" * default-value="armeabi" */ private String nativeLibrariesDependenciesHardwareArchitectureDefault; /** * <p> * Classifier to add to the artifact generated. If given, the artifact will * be an attachment instead. * </p> * * @parameter */ private String classifier; /** * <p> * Override hardware architecture for native library dependencies (with * {@code <type>so</type>}). * </p> * <p> * This overrides any classifier on native library dependencies, and any * {@code nativeLibrariesDependenciesHardwareArchitectureDefault}. * </p> * <p> * Valid values currently include {@code armeabi} and {@code armeabi-v7a}. * </p> * * @parameter expression= * "${android.nativeLibrariesDependenciesHardwareArchitectureOverride}" */ private String nativeLibrariesDependenciesHardwareArchitectureOverride; /** * <p> * Additional source directories that contain resources to be packaged into * the apk. * </p> * <p> * These are not source directories, that contain java classes to be * compiled. It corresponds to the -df option of the apkbuilder program. It * allows you to specify directories, that contain additional resources to * be packaged into the apk. * </p> * So an example inside the plugin configuration could be: * * <pre> * <configuration> * ... * <sourceDirectories> * <sourceDirectory>${project.basedir}/additionals</sourceDirectory> * </sourceDirectories> * ... * </configuration> * </pre> * * @parameter expression="${android.sourceDirectories}" default-value="" */ private File[] sourceDirectories; /** * @component * @readonly * @required */ protected ArtifactFactory artifactFactory; /** * Build location * * @parameter expression="/do_not_checkin/build" */ private String buildDirectory; private File srcDir; private File targetDir; private File buildDir; private File buildInfoFile; private List<BuildInfo> buildInfoList; private int nextBuildNo; private Date currentDate; private String apkFileName; private String deliverable; private static final Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$", 2); @Override public void execute() throws MojoExecutionException, MojoFailureException { // Make an early exit if we're not supposed to generate the APK if (!generateApk) { return; } buildInfoList = new ArrayList<BuildInfo>(); // initialization srcDir = new File(baseDir.getPath() + File.separator + sourceDirectory); buildDir = new File(baseDir.getPath() + buildDirectory); if (!buildDir.exists()) { buildDir.mkdir(); getLog().info("Build directory created..." + buildDir.getPath()); } buildInfoFile = new File(buildDir.getPath() + "/build.info"); // nextBuildNo = generateNextBuildNo(); currentDate = Calendar.getInstance().getTime(); configure(); generateIntermediateAp_(); // Initialize apk build configuration File outputFile = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + '.' + APK); final boolean signWithDebugKeyStore = getAndroidSigner().isSignWithDebugKeyStore(); if (getAndroidSigner().shouldCreateBothSignedAndUnsignedApk()) { getLog().info("Creating debug key signed apk file " + outputFile); createApkFile(outputFile, true); final File unsignedOutputFile = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + "-unsigned." + APK); getLog().info("Creating additional unsigned apk file " + unsignedOutputFile); createApkFile(unsignedOutputFile, false); projectHelper.attachArtifact(project, unsignedOutputFile, classifier == null ? "unsigned" : classifier + "_unsigned"); } else { createApkFile(outputFile, signWithDebugKeyStore); } if (classifier == null) { // Set the generated .apk file as the main artifact (because the pom // states <packaging>apk</packaging>) project.getArtifact().setFile(outputFile); } else { // If there is a classifier specified, attach the artifact using that projectHelper.attachArtifact(project, outputFile, classifier); } /*if (outputFile.exists()) { try { getLog().info("APK created.. Copying to Build directory....."); String buildName = project.getBuild().getFinalName() + '_' + getTimeStampForBuildName(currentDate); File destFile = new File(buildDir, buildName + '.' + APK); FileUtils.copyFile(outputFile, destFile); getLog().info("copied to..." + destFile.getName()); apkFileName = destFile.getName(); getLog().info("Creating deliverables....."); ZipArchiver zipArchiver = new ZipArchiver(); File inputFile = new File(apkFileName); zipArchiver.addFile(destFile, destFile.getName()); File deliverableZip = new File(buildDir, buildName + ".zip"); zipArchiver.setDestFile(deliverableZip); zipArchiver.createArchive(); deliverable = deliverableZip.getName(); getLog().info("Deliverables available at " + deliverableZip.getName()); writeBuildInfo(true); } catch (IOException e) { throw new MojoExecutionException("Error in writing output..."); } }*/ } private void configure() throws MojoExecutionException { try { if (StringUtils.isEmpty(environmentName)) { return; } getLog().info("Configuring the project...."); File srcConfigFile = new File(sourceDirectory.getParent(), "/assets/phresco-env-config.xml"); String basedir = baseDir.getName(); PluginUtils pu = new PluginUtils(); pu.executeUtil(environmentName, basedir, srcConfigFile); pu.setDefaultEnvironment(environmentName, srcConfigFile); } catch (PhrescoException e) { throw new MojoExecutionException(e.getMessage()); } } /*private int generateNextBuildNo() throws IOException { int nextBuildNo = 1; if (!buildInfoFile.exists()) { return nextBuildNo; } BufferedReader read = new BufferedReader(new FileReader(buildInfoFile)); String content = read.readLine(); Gson gson = new Gson(); java.lang.reflect.Type listType = new TypeToken<List<BuildInfo>>() { }.getType(); buildInfoList = (List<BuildInfo>) gson.fromJson(content, listType); if (buildInfoList == null || buildInfoList.size() == 0) { return nextBuildNo; } int buildArray[] = new int[buildInfoList.size()]; int count = 0; for (BuildInfo buildInfo : buildInfoList) { buildArray[count] = buildInfo.getBuildNo(); count++; } Arrays.sort(buildArray); // sort to the array to find the max build no nextBuildNo = buildArray[buildArray.length - 1] + 1; // increment 1 to the max in the build list return nextBuildNo; }*/ void createApkFile(File outputFile, boolean signWithDebugKeyStore) throws MojoExecutionException { File dexFile = new File(project.getBuild().getDirectory(), "classes.dex"); File zipArchive = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".ap_"); ArrayList<File> sourceFolders = new ArrayList<File>(); if (sourceDirectories != null) { for (File f : sourceDirectories) { sourceFolders.add(f); } } ArrayList<File> jarFiles = new ArrayList<File>(); ArrayList<File> nativeFolders = new ArrayList<File>(); boolean useInternalAPKBuilder = true; try { initializeAPKBuilder(); // Ok... // So we can try to use the internal ApkBuilder } catch (Throwable e) { // Not supported platform try to old way. useInternalAPKBuilder = false; } // Process the native libraries, looking both in the current build // directory as well as // at the dependencies declared in the pom. Currently, all .so files are // automatically included processNativeLibraries(nativeFolders); if (useInternalAPKBuilder) { doAPKWithAPKBuilder(outputFile, dexFile, zipArchive, sourceFolders, jarFiles, nativeFolders, false, signWithDebugKeyStore, false); } else { doAPKWithCommand(outputFile, dexFile, zipArchive, sourceFolders, jarFiles, nativeFolders, signWithDebugKeyStore); } } private final Map<String, List<File>> m_jars = new HashMap<String, List<File>>(); private void computeDuplicateFiles(File jar) throws IOException { ZipFile file = new ZipFile(jar); Enumeration<? extends ZipEntry> list = file.entries(); while (list.hasMoreElements()) { ZipEntry ze = list.nextElement(); if (!(ze.getName().contains("META-INF/") || ze.isDirectory())) { // Exclude // META-INF // and // Directories List<File> l = m_jars.get(ze.getName()); if (l == null) { l = new ArrayList<File>(); m_jars.put(ze.getName(), l); } l.add(jar); } } } /** * Creates the APK file using the internal APKBuilder. * * @param outputFile * the output file * @param dexFile * the dex file * @param zipArchive * the classes folder * @param sourceFolders * the resources * @param jarFiles * the embedded java files * @param nativeFolders * the native folders * @param verbose * enables the verbose mode * @param signWithDebugKeyStore * enables the signature of the APK using the debug key * @param debug * enables the debug mode * @throws MojoExecutionException * if the APK cannot be created. */ private void doAPKWithAPKBuilder(File outputFile, File dexFile, File zipArchive, ArrayList<File> sourceFolders, ArrayList<File> jarFiles, ArrayList<File> nativeFolders, boolean verbose, boolean signWithDebugKeyStore, boolean debug) throws MojoExecutionException { /* Following line doesn't make any difference if we keep it or comment it * Commented By - Viral - Feb 11, 2012 */ // sourceFolders.add(new File(project.getBuild().getDirectory(), "android-classes")); for (Artifact artifact : getRelevantCompileArtifacts()) { if (extractDuplicates) { try { computeDuplicateFiles(artifact.getFile()); } catch (Exception e) { getLog().warn("Cannot compute duplicates files from " + artifact.getFile().getAbsolutePath(), e); } } jarFiles.add(artifact.getFile()); } // Check duplicates. if (extractDuplicates) { List<String> duplicates = new ArrayList<String>(); List<File> jarToModify = new ArrayList<File>(); for (String s : m_jars.keySet()) { List<File> l = m_jars.get(s); if (l.size() > 1) { getLog().warn("Duplicate file " + s + " : " + l); duplicates.add(s); for (int i = 1; i < l.size(); i++) { if (!jarToModify.contains(l.get(i))) { jarToModify.add(l.get(i)); } } } } // Rebuild jars. for (File file : jarToModify) { File newJar; newJar = removeDuplicatesFromJar(file, duplicates); int index = jarFiles.indexOf(file); if (newJar != null) { jarFiles.set(index, newJar); } } } ApkBuilder builder = new ApkBuilder(outputFile, zipArchive, dexFile, signWithDebugKeyStore, (verbose) ? System.out : null); if (debug) { builder.setDebugMode(debug); } /* Following code block is responsible to make the .apk size almost doubled. * Commented By - Viral - Feb 11, 2012 */ // for (File sourceFolder : sourceFolders) { // builder.addSourceFolder(sourceFolder); // } for (File jarFile : jarFiles) { if (jarFile.isDirectory()) { String[] filenames = jarFile.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return PATTERN_JAR_EXT.matcher(name).matches(); } }); for (String filename : filenames) { builder.addResourcesFromJar(new File(jarFile, filename)); } } else { builder.addResourcesFromJar(jarFile); } } for (File nativeFolder : nativeFolders) { builder.addNativeLibraries(nativeFolder, null); } builder.sealApk(); } private File removeDuplicatesFromJar(File in, List<String> duplicates) { File target = new File(project.getBasedir(), "target"); File tmp = new File(target, "unpacked-embedded-jars"); tmp.mkdirs(); File out = new File(tmp, in.getName()); if (out.exists()) { return out; } try { out.createNewFile(); } catch (IOException e) { e.printStackTrace(); } // Create a new Jar file FileOutputStream fos = null; ZipOutputStream jos = null; try { fos = new FileOutputStream(out); jos = new ZipOutputStream(fos); } catch (FileNotFoundException e1) { getLog().error("Cannot remove duplicates : the output file " + out.getAbsolutePath() + " does not found"); return null; } ZipFile inZip = null; try { inZip = new ZipFile(in); Enumeration<? extends ZipEntry> entries = inZip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); // If the entry is not a duplicate, copy. if (!duplicates.contains(entry.getName())) { // copy the entry header to jos jos.putNextEntry(entry); InputStream currIn = inZip.getInputStream(entry); copyStreamWithoutClosing(currIn, jos); currIn.close(); jos.closeEntry(); } } } catch (IOException e) { getLog().error("Cannot removing duplicates : " + e.getMessage()); return null; } try { if (inZip != null) { inZip.close(); } jos.close(); fos.close(); jos = null; fos = null; } catch (IOException e) { // ignore it. } getLog().info(in.getName() + " rewritten without duplicates : " + out.getAbsolutePath()); return out; } /** * Copies an input stream into an output stream but does not close the * streams. * * @param in * the input stream * @param out * the output stream * @throws IOException * if the stream cannot be copied */ private static void copyStreamWithoutClosing(InputStream in, OutputStream out) throws IOException { byte[] b = new byte[4096]; for (int n; (n = in.read(b)) != -1;) { out.write(b, 0, n); } } /** * Creates the APK file using the command line. * * @param outputFile * the output file * @param dexFile * the dex file * @param zipArchive * the classes folder * @param sourceFolders * the resources * @param jarFiles * the embedded java files * @param nativeFolders * the native folders * @param signWithDebugKeyStore * enables the signature of the APK using the debug key * @throws MojoExecutionException * if the APK cannot be created. */ private void doAPKWithCommand(File outputFile, File dexFile, File zipArchive, ArrayList<File> sourceFolders, ArrayList<File> jarFiles, ArrayList<File> nativeFolders, boolean signWithDebugKeyStore) throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger(this.getLog()); List<String> commands = new ArrayList<String>(); commands.add(outputFile.getAbsolutePath()); if (!signWithDebugKeyStore) { commands.add("-u"); } commands.add("-z"); commands.add(new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".ap_").getAbsolutePath()); commands.add("-f"); commands.add(new File(project.getBuild().getDirectory(), "classes.dex").getAbsolutePath()); commands.add("-rf"); commands.add(new File(project.getBuild().getDirectory(), "classes").getAbsolutePath()); if (nativeFolders != null && !nativeFolders.isEmpty()) { for (File lib : nativeFolders) { commands.add("-nf"); commands.add(lib.getAbsolutePath()); } } for (Artifact artifact : getRelevantCompileArtifacts()) { commands.add("-rj"); commands.add(artifact.getFile().getAbsolutePath()); } getLog().info(getAndroidSdk().getPathForTool("apkbuilder") + " " + commands.toString()); try { executor.executeCommand(getAndroidSdk().getPathForTool("apkbuilder"), commands, project.getBasedir(), false); } catch (ExecutionException e) { throw new MojoExecutionException("", e); } } private void initializeAPKBuilder() throws MojoExecutionException { File file = getAndroidSdk().getSDKLibJar(); ApkBuilder.initialize(getLog(), file); } private void processNativeLibraries(final List<File> natives) throws MojoExecutionException { // Examine the native libraries directory for content. This will only be // true if: // a) the directory exists // b) it contains at least 1 file final boolean hasValidNativeLibrariesDirectory = nativeLibrariesDirectory != null && nativeLibrariesDirectory.exists() && (nativeLibrariesDirectory.listFiles() != null && nativeLibrariesDirectory.listFiles().length > 0); // Retrieve any native dependencies or attached artifacts. This may // include artifacts from the ndk-build MOJO NativeHelper nativeHelper = new NativeHelper(project, projectRepos, repoSession, repoSystem, artifactFactory, getLog()); final Set<Artifact> artifacts = nativeHelper.getNativeDependenciesArtifacts(unpackedApkLibsDirectory, true); final boolean hasValidBuildNativeLibrariesDirectory = nativeLibrariesOutputDirectory.exists() && (nativeLibrariesOutputDirectory.listFiles() != null && nativeLibrariesOutputDirectory.listFiles().length > 0); if (artifacts.isEmpty() && hasValidNativeLibrariesDirectory && !hasValidBuildNativeLibrariesDirectory) { getLog().debug("No native library dependencies detected, will point directly to " + nativeLibrariesDirectory); // Point directly to the directory in this case - no need to copy // files around natives.add(nativeLibrariesDirectory); } else if (!artifacts.isEmpty() || hasValidNativeLibrariesDirectory) { // In this case, we may have both .so files in it's normal location // as well as .so dependencies // Create the ${project.build.outputDirectory}/libs final File destinationDirectory = new File(nativeLibrariesOutputDirectory.getAbsolutePath()); destinationDirectory.mkdirs(); // Point directly to the directory natives.add(destinationDirectory); // If we have a valid native libs, copy those files - these already // come in the structure required if (hasValidNativeLibrariesDirectory) { copyLocalNativeLibraries(nativeLibrariesDirectory, destinationDirectory); } if (!artifacts.isEmpty()) { for (Artifact resolvedArtifact : artifacts) { if ("so".equals(resolvedArtifact.getType())) { final File artifactFile = resolvedArtifact.getFile(); try { final String artifactId = resolvedArtifact.getArtifactId(); final String filename = artifactId.startsWith("lib") ? artifactId + ".so" : "lib" + artifactId + ".so"; final File finalDestinationDirectory = getFinalDestinationDirectoryFor(resolvedArtifact, destinationDirectory); final File file = new File(finalDestinationDirectory, filename); getLog().debug("Copying native dependency " + artifactId + " (" + resolvedArtifact.getGroupId() + ") to " + file); org.apache.commons.io.FileUtils.copyFile(artifactFile, file); } catch (Exception e) { throw new MojoExecutionException("Could not copy native dependency.", e); } } else if (APKLIB.equals(resolvedArtifact.getType())) { natives.add(new File(getLibraryUnpackDirectory(resolvedArtifact) + "/libs")); } } } } } private File getFinalDestinationDirectoryFor(Artifact resolvedArtifact, File destinationDirectory) { final String hardwareArchitecture = getHardwareArchitectureFor(resolvedArtifact); File finalDestinationDirectory = new File(destinationDirectory, hardwareArchitecture + "/"); finalDestinationDirectory.mkdirs(); return finalDestinationDirectory; } private String getHardwareArchitectureFor(Artifact resolvedArtifact) { if (StringUtils.isNotBlank(nativeLibrariesDependenciesHardwareArchitectureOverride)) { return nativeLibrariesDependenciesHardwareArchitectureOverride; } final String classifier = resolvedArtifact.getClassifier(); if (StringUtils.isNotBlank(classifier)) { return classifier; } return nativeLibrariesDependenciesHardwareArchitectureDefault; } private void copyLocalNativeLibraries(final File localNativeLibrariesDirectory, final File destinationDirectory) throws MojoExecutionException { getLog().debug("Copying existing native libraries from " + localNativeLibrariesDirectory); try { org.apache.commons.io.FileUtils.copyDirectory(localNativeLibrariesDirectory, destinationDirectory, new FileFilter() { @Override public boolean accept(final File pathname) { return pathname.getName().endsWith(".so"); } }); } catch (IOException e) { getLog().error("Could not copy native libraries: " + e.getMessage(), e); throw new MojoExecutionException("Could not copy native dependency.", e); } } /** * Generates an intermediate apk file (actually .ap_) containing the * resources and assets. * * @throws MojoExecutionException */ private void generateIntermediateAp_() throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger(this.getLog()); File[] overlayDirectories; if (resourceOverlayDirectories == null || resourceOverlayDirectories.length == 0) { overlayDirectories = new File[] { resourceOverlayDirectory }; } else { overlayDirectories = resourceOverlayDirectories; } if (extractedDependenciesRes.exists()) { try { getLog().info("Copying dependency resource files to combined resource directory."); if (!combinedRes.exists() && !combinedRes.mkdirs()) { throw new MojoExecutionException("Could not create directory for combined resources at " + combinedRes.getAbsolutePath()); } org.apache.commons.io.FileUtils.copyDirectory(extractedDependenciesRes, combinedRes); } catch (IOException e) { throw new MojoExecutionException("", e); } } if (resourceDirectory.exists() && combinedRes.exists()) { try { getLog().info("Copying local resource files to combined resource directory."); org.apache.commons.io.FileUtils.copyDirectory(resourceDirectory, combinedRes, new FileFilter() { /** * Excludes files matching one of the common file to * exclude. The default excludes pattern are the ones from * {org * .codehaus.plexus.util.AbstractScanner#DEFAULTEXCLUDES} * * @see java.io.FileFilter#accept(java.io.File) */ @Override public boolean accept(File file) { for (String pattern : AbstractScanner.DEFAULTEXCLUDES) { if (AbstractScanner.match(pattern, file.getAbsolutePath())) { getLog().debug("Excluding " + file.getName() + " from resource copy : matching " + pattern); return false; } } return true; } }); } catch (IOException e) { throw new MojoExecutionException("", e); } } // Must combine assets. // The aapt tools does not support several -A arguments. // We copy the assets from extracted dependencies first, and then the // local assets. // This allows redefining the assets in the current project if (extractedDependenciesAssets.exists()) { try { getLog().info("Copying dependency assets files to combined assets directory."); org.apache.commons.io.FileUtils.copyDirectory(extractedDependenciesAssets, combinedAssets, new FileFilter() { /** * Excludes files matching one of the common file to * exclude. The default excludes pattern are the ones from * {org * .codehaus.plexus.util.AbstractScanner#DEFAULTEXCLUDES} * * @see java.io.FileFilter#accept(java.io.File) */ @Override public boolean accept(File file) { for (String pattern : AbstractScanner.DEFAULTEXCLUDES) { if (AbstractScanner.match(pattern, file.getAbsolutePath())) { getLog().debug("Excluding " + file.getName() + " from asset copy : matching " + pattern); return false; } } return true; } }); } catch (IOException e) { throw new MojoExecutionException("", e); } } // Next pull APK Lib assets, reverse the order to give precedence to // libs higher up the chain List<Artifact> artifactList = new ArrayList<Artifact>(getAllRelevantDependencyArtifacts()); for (Artifact artifact : artifactList) { if (artifact.getType().equals(APKLIB)) { File apklibAsssetsDirectory = new File(getLibraryUnpackDirectory(artifact) + "/assets"); if (apklibAsssetsDirectory.exists()) { try { getLog().info("Copying dependency assets files to combined assets directory."); org.apache.commons.io.FileUtils.copyDirectory(apklibAsssetsDirectory, combinedAssets, new FileFilter() { /** * Excludes files matching one of the common file to * exclude. The default excludes pattern are the * ones from * {org.codehaus.plexus.util.AbstractScanner * #DEFAULTEXCLUDES} * * @see java.io.FileFilter#accept(java.io.File) */ @Override public boolean accept(File file) { for (String pattern : AbstractScanner.DEFAULTEXCLUDES) { if (AbstractScanner.match(pattern, file.getAbsolutePath())) { getLog().debug("Excluding " + file.getName() + " from asset copy : matching " + pattern); return false; } } return true; } }); } catch (IOException e) { throw new MojoExecutionException("", e); } } } } if (assetsDirectory.exists()) { try { getLog().info("Copying local assets files to combined assets directory."); org.apache.commons.io.FileUtils.copyDirectory(assetsDirectory, combinedAssets, new FileFilter() { /** * Excludes files matching one of the common file to * exclude. The default excludes pattern are the ones from * {org * .codehaus.plexus.util.AbstractScanner#DEFAULTEXCLUDES} * * @see java.io.FileFilter#accept(java.io.File) */ @Override public boolean accept(File file) { for (String pattern : AbstractScanner.DEFAULTEXCLUDES) { if (AbstractScanner.match(pattern, file.getAbsolutePath())) { getLog().debug("Excluding " + file.getName() + " from asset copy : matching " + pattern); return false; } } return true; } }); } catch (IOException e) { throw new MojoExecutionException("", e); } } File androidJar = getAndroidSdk().getAndroidJar(); File outputFile = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".ap_"); List<String> commands = new ArrayList<String>(); commands.add("package"); commands.add("-f"); commands.add("-M"); commands.add(androidManifestFile.getAbsolutePath()); for (File resOverlayDir : overlayDirectories) { if (resOverlayDir != null && resOverlayDir.exists()) { commands.add("-S"); commands.add(resOverlayDir.getAbsolutePath()); } } if (combinedRes.exists()) { commands.add("-S"); commands.add(combinedRes.getAbsolutePath()); } else { if (resourceDirectory.exists()) { commands.add("-S"); commands.add(resourceDirectory.getAbsolutePath()); } } for (Artifact artifact : getAllRelevantDependencyArtifacts()) { if (artifact.getType().equals(APKLIB)) { final String apkLibResDir = getLibraryUnpackDirectory(artifact) + "/res"; if (new File(apkLibResDir).exists()) { commands.add("-S"); commands.add(apkLibResDir); } } } commands.add("--auto-add-overlay"); // Use the combined assets. // Indeed, aapt does not support several -A arguments. if (combinedAssets.exists()) { commands.add("-A"); commands.add(combinedAssets.getAbsolutePath()); } if (StringUtils.isNotBlank(renameManifestPackage)) { commands.add("--rename-manifest-package"); commands.add(renameManifestPackage); } if (StringUtils.isNotBlank(renameInstrumentationTargetPackage)) { commands.add("--rename-instrumentation-target-package"); commands.add(renameInstrumentationTargetPackage); } commands.add("-I"); commands.add(androidJar.getAbsolutePath()); commands.add("-F"); commands.add(outputFile.getAbsolutePath()); if (StringUtils.isNotBlank(configurations)) { commands.add("-c"); commands.add(configurations); } for (String aaptExtraArg : aaptExtraArgs) { commands.add(aaptExtraArg); } getLog().info(getAndroidSdk().getPathForTool("aapt") + " " + commands.toString()); try { executor.executeCommand(getAndroidSdk().getPathForTool("aapt"), commands, project.getBasedir(), false); } catch (ExecutionException e) { throw new MojoExecutionException("", e); } } protected AndroidSigner getAndroidSigner() { if (sign == null) { return new AndroidSigner(signDebug); } return new AndroidSigner(sign.getDebug()); } }