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

com.simpligility.maven.plugins.android.phase09package.ApkMojo Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2009 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.phase09package;

import com.android.sdklib.build.ApkBuilder;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.build.SealedApkException;
import com.google.common.io.Files;
import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.AndroidNdk;
import com.simpligility.maven.plugins.android.AndroidSigner;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.IncludeExcludeSet;
import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
import com.simpligility.maven.plugins.android.common.AndroidExtension;
import com.simpligility.maven.plugins.android.common.NativeHelper;
import com.simpligility.maven.plugins.android.config.ConfigHandler;
import com.simpligility.maven.plugins.android.config.ConfigPojo;
import com.simpligility.maven.plugins.android.config.PullParameter;
import com.simpligility.maven.plugins.android.configuration.Apk;
import com.simpligility.maven.plugins.android.configuration.MetaInf;
import com.simpligility.maven.plugins.android.configuration.Sign;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.FileFileFilter;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.plugins.shade.resource.ResourceTransformer;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
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.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarOutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;




/**
 * Creates the apk file. By default signs it with debug keystore.
* Change that by setting configuration parameter * <sign><debug>false</debug></sign>. * * @author [email protected] */ @Mojo( name = "apk", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE ) public class ApkMojo extends AbstractAndroidMojo { /** *

How to sign the apk.

*

Looks like this:

*
     * <sign>
     *     <debug>auto</debug>
     * </sign>
     * 
*

Valid values for <debug> are: *

    *
  • true = sign with the debug keystore. *
  • false = don't sign with the debug keystore. *
  • both = create a signed as well as an unsigned apk. *
  • auto (default) = sign with debug keystore, unless another keystore is defined. (Signing with * other keystores is not yet implemented. See * Issue 2.) *

*

Can also be configured from command-line with parameter -Dandroid.sign.debug.

*/ @Parameter private Sign sign; /** *

Parameter designed to pick up -Dandroid.sign.debug in case there is no pom with a * <sign> configuration tag.

*

Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Sign#debug}.

*/ @Parameter( property = "android.sign.debug", defaultValue = "auto", readonly = true ) private String signDebug; /** *

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.

* * TODO pass this into AaptExecutor */ @Parameter( property = "android.renameInstrumentationTargetPackage" ) private String renameInstrumentationTargetPackage; /** *

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( property = "android.extractDuplicates", defaultValue = "false" ) private boolean extractDuplicates; /** *

Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.

*/ @Parameter private String classifier; /** * The apk file produced by the apk goal. Per default the file is placed into the build directory (target * normally) using the build final name and apk as extension. */ @Parameter( property = "android.outputApk", defaultValue = "${project.build.directory}/${project.build.finalName}.apk" ) private String outputApk; /** *

Additional source directories that contain resources to be packaged into the apk.

*

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.

* So an example inside the plugin configuration could be: *
     * <configuration>
     *   ...
     *    <sourceDirectories>
     *      <sourceDirectory>${project.basedir}/additionals</sourceDirectory>
     *   </sourceDirectories>
     *   ...
     * </configuration>
     * 
*/ @Parameter( property = "android.sourceDirectories" ) private File[] sourceDirectories; /** * Pattern for additional META-INF resources to be packaged into the apk. *

* The APK builder filters these resources and doesn't include them into the apk. * This leads to bad behaviour of dependent libraries relying on these resources, * for instance service discovery doesn't work.
* By specifying this pattern, the android plugin adds these resources to the final apk. *

*

The pattern is relative to META-INF, i.e. one must use *

     * 
     * <apkMetaIncludes>
     *     <metaInclude>services/**</metaInclude>
     * </apkMetaIncludes>
     * 
     * 
* ... instead of *
     * 
     * <apkMetaIncludes>
     *     <metaInclude>META-INF/services/**</metaInclude>
     * </apkMetaIncludes>
     * 
     * 
*

* See also Issue 97 *

* * @deprecated in favour of apk.metaInf */ @PullParameter private String[] apkMetaIncludes; @PullParameter( defaultValueGetterMethod = "getDefaultMetaInf" ) private MetaInf apkMetaInf; @Parameter( alias = "metaInf" ) private MetaInf pluginMetaInf; /** * Defines whether or not the APK is being produced in debug mode or not. */ @Parameter( property = "android.apk.debug" ) @PullParameter( defaultValue = "false" ) private Boolean apkDebug; @Parameter( property = "android.nativeToolchain" ) @PullParameter( defaultValue = "arm-linux-androideabi-4.4.3" ) private String apkNativeToolchain; /** * Specifies the final name of the library output by the build (this allows */ @Parameter( property = "android.ndk.build.build.final-library.name" ) private String ndkFinalLibraryName; /** * Specify a list of patterns that are matched against the names of jar file * dependencies. Matching jar files will not have their resources added to the * resulting APK. * * The patterns are standard Java regexes. */ @Parameter private String[] excludeJarResources; private Pattern[] excludeJarResourcesPatterns; /** * Embedded configuration of this mojo. */ @Parameter @ConfigPojo( prefix = "apk" ) private Apk apk; /** * Skips transitive dependencies. May be useful if the target classes directory is populated with the * {@code maven-dependency-plugin} and already contains all dependency classes. */ @Parameter( property = "skipDependencies", defaultValue = "false" ) private boolean skipDependencies; /** * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty * strings are ignored. Example: *
     *     <artifactTypeSet>
     *         <includes>
     *             <include>aar</include>
     *         <includes>
     *         <excludes>
     *             <exclude>jar</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     * 
*/ @Parameter( property = "artifactTypeSet" ) private IncludeExcludeSet artifactTypeSet; /** * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId}, * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact * range. Empty strings are ignored. Example: *
     *     <artifactTypeSet>
     *         <includes>
     *             <include>foo-group:foo-artifact:1.0-SNAPSHOT</include>
     *             <include>bar-group:bar-artifact:1.0-SNAPSHOT</include>
     *             <include>baz-group:*</include>
     *         <includes>
     *         <excludes>
     *             <exclude>qux-group:qux-artifact:*</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     * 
*/ @Parameter( property = "artifactSet" ) private IncludeExcludeSet artifactSet; private static final Pattern PATTERN_JAR_EXT = Pattern.compile( "^.+\\.jar$", Pattern.CASE_INSENSITIVE ); private static final String DEX_SUFFIX = ".dex"; private static final String CLASSES = "classes"; /** *

Default hardware architecture for native library dependencies (with {@code <type>so</type>}) * without a classifier.

*

Valid values currently include {@code armeabi}, {@code armeabi-v7a}, {@code mips} and {@code x86}.

*/ @Parameter( property = "android.nativeLibrariesDependenciesHardwareArchitectureDefault", defaultValue = "armeabi" ) private String nativeLibrariesDependenciesHardwareArchitectureDefault; @Parameter private ResourceTransformer[] transformers; /** * @throws MojoExecutionException * @throws MojoFailureException */ public void execute() throws MojoExecutionException, MojoFailureException { // Make an early exit if we're not supposed to generate the APK if ( ! generateApk ) { return; } ConfigHandler cfh = new ConfigHandler( this, this.session, this.execution ); cfh.parseConfiguration(); generateIntermediateApk(); // Compile resource exclusion patterns, if any if ( excludeJarResources != null && excludeJarResources.length > 0 ) { getLog().debug( "Compiling " + excludeJarResources.length + " patterns" ); excludeJarResourcesPatterns = new Pattern[excludeJarResources.length]; for ( int index = 0; index < excludeJarResources.length; ++index ) { excludeJarResourcesPatterns[index] = Pattern.compile( excludeJarResources[index] ); } } // Initialize apk build configuration File outputFile = new File( outputApk ); 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( targetDirectory, finalName + "-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 apk) project.getArtifact().setFile( outputFile ); } else { // If there is a classifier specified, attach the artifact using that projectHelper.attachArtifact( project, AndroidExtension.APK, classifier, outputFile ); } } void createApkFile( File outputFile, boolean signWithDebugKeyStore ) throws MojoExecutionException { //this needs to come from DexMojo File dexFile = new File( targetDirectory, "classes.dex" ); if ( !dexFile.exists() ) { dexFile = new File( targetDirectory, "classes.zip" ); } File zipArchive = new File( targetDirectory, finalName + ".ap_" ); ArrayList sourceFolders = new ArrayList(); if ( sourceDirectories != null ) { sourceFolders.addAll( Arrays.asList( sourceDirectories ) ); } ArrayList jarFiles = new ArrayList(); // 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 final Collection nativeFolders = getNativeLibraryFolders(); getLog().info( "Adding native libraries : " + nativeFolders ); doAPKWithAPKBuilder( outputFile, dexFile, zipArchive, sourceFolders, jarFiles, nativeFolders, signWithDebugKeyStore ); if ( this.apkMetaInf != null ) { File outputJar = new File( outputApk.substring( 0, outputApk.length() - 3 ) + "jar" ); if ( outputJar.exists() ) { jarFiles.add( outputJar ); } else { getLog().warn( "Output jar doesn't exist:" + outputJar ); } try { addMetaInf( outputFile, jarFiles ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not add META-INF resources.", e ); } } } private void addMetaInf( File outputFile, ArrayList jarFiles ) throws IOException { File tmp = File.createTempFile( outputFile.getName(), ".add", outputFile.getParentFile() ); FileOutputStream fos = new FileOutputStream( tmp ); JarOutputStream zos = new JarOutputStream( fos ); Set entries = new HashSet(); updateWithMetaInf( zos, outputFile, entries, false ); for ( File f : jarFiles ) { updateWithMetaInf( zos, f, entries, true ); } if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.hasTransformedResource() ) { transformer.modifyOutputStream( zos ); } } } zos.close(); outputFile.delete(); if ( ! tmp.renameTo( outputFile ) ) { throw new IOException( String.format( "Cannot rename %s to %s", tmp, outputFile.getName() ) ); } } private void updateWithMetaInf( ZipOutputStream zos, File jarFile, Set entries, boolean metaInfOnly ) throws IOException { ZipFile zin = new ZipFile( jarFile ); for ( Enumeration en = zin.entries(); en.hasMoreElements(); ) { ZipEntry ze = en.nextElement(); if ( ze.isDirectory() ) { continue; } String zn = ze.getName(); if ( metaInfOnly ) { if ( ! zn.startsWith( "META-INF/" ) ) { continue; } if ( ! this.apkMetaInf.isIncluded( zn ) ) { continue; } } boolean resourceTransformed = false; if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.canTransformResource( zn ) ) { getLog().info( "Transforming " + zn + " using " + transformer.getClass().getName() ); InputStream is = zin.getInputStream( ze ); transformer.processResource( zn, is, null ); is.close(); resourceTransformed = true; break; } } } if ( !resourceTransformed ) { // Avoid duplicates that aren't accounted for by the resource transformers if ( metaInfOnly && this.extractDuplicates && ! entries.add( zn ) ) { continue; } InputStream is = zin.getInputStream( ze ); final ZipEntry ne; if ( ze.getMethod() == ZipEntry.STORED ) { ne = new ZipEntry( ze ); } else { ne = new ZipEntry( zn ); } zos.putNextEntry( ne ); copyStreamWithoutClosing( is, zos ); is.close(); zos.closeEntry(); } } zin.close(); } private Map> jars = new HashMap>(); private void computeDuplicateFiles( File jar ) throws IOException { ZipFile file = new ZipFile( jar ); Enumeration list = file.entries(); while ( list.hasMoreElements() ) { ZipEntry ze = list.nextElement(); if ( ! ( ze.getName().contains( "META-INF/" ) || ze.isDirectory() ) ) { // Exclude META-INF and Directories List l = jars.get( ze.getName() ); if ( l == null ) { l = new ArrayList(); jars.put( ze.getName(), l ); } l.add( jar ); } } } private void computeDuplicateFilesInSource( File folder ) { String rPath = folder.getAbsolutePath(); for ( File file : Files.fileTreeTraverser().breadthFirstTraversal( folder ).toList() ) { String lPath = file.getAbsolutePath(); if ( lPath.equals( rPath ) ) { continue; //skip the root } lPath = lPath.substring( rPath.length() + 1 ); //strip root folder to make relative path if ( jars.get( lPath ) == null ) { jars.put( lPath, new ArrayList() ); } jars.get( lPath ).add( folder ); } } private void extractDuplicateFiles( List jarFiles, Collection sourceFolders ) throws IOException { getLog().debug( "Extracting duplicates" ); List duplicates = new ArrayList(); List jarToModify = new ArrayList(); for ( String s : jars.keySet() ) { List l = jars.get( s ); if ( l.size() > 1 ) { getLog().warn( "Duplicate file " + s + " : " + l ); duplicates.add( s ); for ( int i = 0; i < l.size(); i++ ) { if ( ! jarToModify.contains( l.get( i ) ) ) { jarToModify.add( l.get( i ) ); } } } } // Rebuild jars. Remove duplicates from ALL jars, then add them back into a duplicate-resources.jar File tmp = new File( targetDirectory.getAbsolutePath(), "unpacked-embedded-jars" ); tmp.mkdirs(); File duplicatesJar = new File( tmp, "duplicate-resources.jar" ); Set duplicatesAdded = new HashSet(); duplicatesJar.createNewFile(); final FileOutputStream fos = new FileOutputStream( duplicatesJar ); final JarOutputStream zos = new JarOutputStream( fos ); for ( File file : jarToModify ) { final int index = jarFiles.indexOf( file ); if ( index != -1 ) { final File newJar = removeDuplicatesFromJar( file, duplicates, duplicatesAdded, zos, index ); getLog().debug( "Removed duplicates from " + newJar ); if ( newJar != null ) { jarFiles.set( index, newJar ); } } else { removeDuplicatesFromFolder( file, file, duplicates, duplicatesAdded, zos ); getLog().debug( "Removed duplicates from " + file ); } } //add transformed resources to duplicate-resources.jar if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.hasTransformedResource() ) { transformer.modifyOutputStream( zos ); } } } zos.close(); fos.close(); if ( !jarToModify.isEmpty() && duplicatesJar.length() > 0 ) { jarFiles.add( duplicatesJar ); } } /** * 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 signWithDebugKeyStore enables the signature of the APK using the debug key * @throws MojoExecutionException if the APK cannot be created. */ private void doAPKWithAPKBuilder( File outputFile, File dexFile, File zipArchive, Collection sourceFolders, List jarFiles, Collection nativeFolders, boolean signWithDebugKeyStore ) throws MojoExecutionException { getLog().debug( "Building APK with internal APKBuilder" ); //A when jack is running the classes directory will not get filled (usually) // so let's skip it if it wasn't created by something else if ( projectOutputDirectory.exists() || !getJack().isEnabled() ) { sourceFolders.add( projectOutputDirectory ); } for ( Artifact artifact : filterArtifacts( getRelevantCompileArtifacts(), skipDependencies, artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(), artifactSet.getExcludes() ) ) { getLog().debug( "Found artifact for APK :" + artifact ); if ( extractDuplicates ) { try { computeDuplicateFiles( artifact.getFile() ); } catch ( Exception e ) { getLog().warn( "Cannot compute duplicates files from " + artifact.getFile().getAbsolutePath(), e ); } } jarFiles.add( artifact.getFile() ); } for ( File src : sourceFolders ) { computeDuplicateFilesInSource( src ); } // Check duplicates. if ( extractDuplicates ) { try { extractDuplicateFiles( jarFiles, sourceFolders ); } catch ( IOException e ) { getLog().error( "Could not extract duplicates to duplicate-resources.jar", e ); } } try { final String debugKeyStore = signWithDebugKeyStore ? ApkBuilder.getDebugKeystore() : null; final ApkBuilder apkBuilder = new ApkBuilder( outputFile, zipArchive, dexFile, debugKeyStore, null ); if ( apkDebug ) { apkBuilder.setDebugMode( true ); } for ( File sourceFolder : sourceFolders ) { getLog().debug( "Adding source folder : " + sourceFolder ); // Use ApkBuilder#addFile() to explicitly add resource files so that we can add META-INF/services. addResourcesFromFolder( apkBuilder, sourceFolder ); } for ( File jarFile : jarFiles ) { boolean excluded = false; if ( excludeJarResourcesPatterns != null ) { final String name = jarFile.getName(); getLog().debug( "Checking " + name + " against patterns" ); for ( Pattern pattern : excludeJarResourcesPatterns ) { final Matcher matcher = pattern.matcher( name ); if ( matcher.matches() ) { getLog().debug( "Jar " + name + " excluded by pattern " + pattern ); excluded = true; break; } else { getLog().debug( "Jar " + name + " not excluded by pattern " + pattern ); } } } if ( excluded ) { continue; } if ( jarFile.isDirectory() ) { getLog().debug( "Adding resources from jar folder : " + jarFile ); final String[] filenames = jarFile.list( new FilenameFilter() { public boolean accept( File dir, String name ) { return PATTERN_JAR_EXT.matcher( name ).matches(); } } ); for ( String filename : filenames ) { final File innerJar = new File( jarFile, filename ); getLog().debug( "Adding resources from innerJar : " + innerJar ); apkBuilder.addResourcesFromJar( innerJar ); } } else { getLog().debug( "Adding resources from : " + jarFile ); apkBuilder.addResourcesFromJar( jarFile ); } } addSecondaryDexes( dexFile, apkBuilder ); for ( File nativeFolder : nativeFolders ) { getLog().debug( "Adding native library : " + nativeFolder ); apkBuilder.addNativeLibraries( nativeFolder ); } apkBuilder.sealApk(); } catch ( ApkCreationException | SealedApkException | IOException e ) { throw new MojoExecutionException( e.getMessage(), e ); } catch ( DuplicateFileException e ) { final String msg = String.format( "Duplicated file: %s, found in archive %s and %s", e.getArchivePath(), e.getFile1(), e.getFile2() ); throw new MojoExecutionException( msg, e ); } } /** * Collect all Files from Folder (recursively) that are not class files. */ private void collectFiles( File folder, final List collectedFiles ) { folder.listFiles( new FileFilter() { @Override public boolean accept( File file ) { if ( file.isDirectory() ) { collectFiles( file, collectedFiles ); } else if ( file.isFile() ) { if ( !file.getName().endsWith( ".class" ) ) { collectedFiles.add( file ); } } return false; } } ); } /** * Adds all non-class files from folder, so that we can add META-INF/services resources. */ private void addResourcesFromFolder( ApkBuilder builder, File folder ) throws SealedApkException, DuplicateFileException, ApkCreationException, IOException { final int folderPathLength = folder.getCanonicalPath().length(); final List resourceFiles = new ArrayList<>( ); collectFiles( folder, resourceFiles ); for ( final File resourceFile : resourceFiles ) { final String resourceName = resourceFile .getCanonicalPath() .substring( folderPathLength + 1 ) .replaceAll( "\\\\", "/" ); getLog().info( "Adding resource " + resourceFile + " : " + resourceName ); builder.addFile( resourceFile, resourceName ); } } private void addSecondaryDexes( File dexFile, ApkBuilder apkBuilder ) throws ApkCreationException, SealedApkException, DuplicateFileException { int dexNumber = 2; String dexFileName = getNextDexFileName( dexNumber ); File secondDexFile = createNextDexFile( dexFile, dexFileName ); while ( secondDexFile.exists() ) { apkBuilder.addFile( secondDexFile, dexFileName ); dexNumber++; dexFileName = getNextDexFileName( dexNumber ); secondDexFile = createNextDexFile( dexFile, dexFileName ); } } private File createNextDexFile( File dexFile, String dexFileName ) { return new File( dexFile.getParentFile(), dexFileName ); } private String getNextDexFileName( int dexNumber ) { return CLASSES + dexNumber + DEX_SUFFIX; } private File removeDuplicatesFromJar( File in, List duplicates, Set duplicatesAdded, ZipOutputStream duplicateZos, int num ) { String target = targetDirectory.getAbsolutePath(); File tmp = new File( target, "unpacked-embedded-jars" ); tmp.mkdirs(); String jarName = String.format( "%s-%d.%s", Files.getNameWithoutExtension( in.getName() ), num, Files.getFileExtension( in.getName() ) ); File out = new File( tmp, jarName ); if ( out.exists() ) { return out; } else { try { out.createNewFile(); } catch ( IOException e ) { e.printStackTrace(); } } // Create a new Jar file final FileOutputStream fos; final ZipOutputStream jos; 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; } final ZipFile inZip; try { inZip = new ZipFile( in ); Enumeration 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(); } //if it is duplicate, check the resource transformers else { boolean resourceTransformed = false; if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.canTransformResource( entry.getName() ) ) { getLog().info( "Transforming " + entry.getName() + " using " + transformer.getClass().getName() ); InputStream currIn = inZip.getInputStream( entry ); transformer.processResource( entry.getName(), currIn, null ); currIn.close(); resourceTransformed = true; break; } } } //if not handled by transformer, add (once) to duplicates jar if ( !resourceTransformed ) { if ( !duplicatesAdded.contains( entry.getName() ) ) { duplicatesAdded.add( entry.getName() ); duplicateZos.putNextEntry( entry ); InputStream currIn = inZip.getInputStream( entry ); copyStreamWithoutClosing( currIn, duplicateZos ); currIn.close(); duplicateZos.closeEntry(); } } } } } catch ( IOException e ) { getLog().error( "Cannot removing duplicates : " + e.getMessage() ); return null; } try { inZip.close(); jos.close(); fos.close(); } catch ( IOException e ) { // ignore it. } getLog().info( in.getName() + " rewritten without duplicates : " + out.getAbsolutePath() ); return out; } private void removeDuplicatesFromFolder( File root, File in, List duplicates, Set duplicatesAdded, ZipOutputStream duplicateZos ) { String rPath = root.getAbsolutePath(); try { for ( File f : in.listFiles() ) { if ( f.isDirectory() ) { removeDuplicatesFromFolder( root, f, duplicates, duplicatesAdded, duplicateZos ); } else { String lName = f.getAbsolutePath(); lName = lName.substring( rPath.length() + 1 ); //make relative path if ( duplicates.contains( lName ) ) { boolean resourceTransformed = false; if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.canTransformResource( lName ) ) { getLog().info( "Transforming " + lName + " using " + transformer.getClass().getName() ); InputStream currIn = new FileInputStream( f ); transformer.processResource( lName, currIn, null ); currIn.close(); resourceTransformed = true; break; } } } //if not handled by transformer, add (once) to duplicates jar if ( !resourceTransformed ) { if ( !duplicatesAdded.contains( lName ) ) { duplicatesAdded.add( lName ); ZipEntry entry = new ZipEntry( lName ); duplicateZos.putNextEntry( entry ); InputStream currIn = new FileInputStream( f ); copyStreamWithoutClosing( currIn, duplicateZos ); currIn.close(); duplicateZos.closeEntry(); } } f.delete(); } } } } catch ( IOException e ) { getLog().error( "Cannot removing duplicates : " + e.getMessage() ); } } /** * 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 { final int bufferSize = 4096; byte[] b = new byte[ bufferSize ]; int n; while ( ( n = in.read( b ) ) != - 1 ) { out.write( b, 0, n ); } } private Collection getNativeLibraryFolders() throws MojoExecutionException { final List natives = new ArrayList(); if ( nativeLibrariesDirectory.exists() ) { // If we have prebuilt native libs then copy them over to the native output folder. // NB they will be copied over the top of any native libs generated as part of the NdkBuildMojo copyLocalNativeLibraries( nativeLibrariesDirectory, ndkOutputDirectory ); } final Set artifacts = getNativeLibraryArtifacts(); for ( Artifact resolvedArtifact : artifacts ) { if ( APKLIB.equals( resolvedArtifact.getType() ) || AAR.equals( resolvedArtifact.getType() ) ) { // If the artifact is an AAR or APKLIB then add their native libs folder to the result. final File folder = getUnpackedLibNativesFolder( resolvedArtifact ); getLog().debug( "Adding native library folder " + folder ); natives.add( folder ); } // Copy the native lib dependencies into the native lib output folder for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES ) { if ( NativeHelper.artifactHasHardwareArchitecture( resolvedArtifact, ndkArchitecture, nativeLibrariesDependenciesHardwareArchitectureDefault ) ) { // If the artifact is a native lib then copy it into the native libs output folder. copyNativeLibraryArtifact( resolvedArtifact, ndkOutputDirectory, ndkArchitecture ); } } } if ( apkDebug ) { // Copy the gdbserver binary into the native libs output folder (for each architecture). for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES ) { copyGdbServer( ndkOutputDirectory, ndkArchitecture ); } } if ( ndkOutputDirectory.exists() ) { // If we have any native libs in the native output folder then add the output folder to the result. getLog().debug( "Adding built native library folder " + ndkOutputDirectory ); natives.add( ndkOutputDirectory ); } return natives; } /** * @return Any native dependencies or attached artifacts. This may include artifacts from the ndk-build MOJO. * @throws MojoExecutionException */ private Set getNativeLibraryArtifacts() throws MojoExecutionException { return getNativeHelper().getNativeDependenciesArtifacts( this, getUnpackedLibsDirectory(), true ); } private void copyNativeLibraryArtifact( Artifact artifact, File destinationDirectory, String ndkArchitecture ) throws MojoExecutionException { final File artifactFile = getArtifactResolverHelper().resolveArtifactToFile( artifact ); try { final String artifactId = artifact.getArtifactId(); String filename = artifactId.startsWith( "lib" ) ? artifactId + ".so" : "lib" + artifactId + ".so"; if ( ndkFinalLibraryName != null && artifact.getFile().getName().startsWith( "lib" + ndkFinalLibraryName ) ) { // The artifact looks like one we built with the NDK in this module // preserve the name from the NDK build filename = artifact.getFile().getName(); } final File folder = new File( destinationDirectory, ndkArchitecture ); final File file = new File( folder, filename ); getLog().debug( "Copying native dependency " + artifactId + " (" + artifact.getGroupId() + ") to " + file ); FileUtils.copyFile( artifactFile, file ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not copy native dependency.", e ); } } /** * Copy the Ndk GdbServer into the architecture output folder if the folder exists but the GdbServer doesn't. */ private void copyGdbServer( File destinationDirectory, String architecture ) throws MojoExecutionException { try { final File destDir = new File( destinationDirectory, architecture ); if ( destDir.exists() ) { // Copy the gdbserver binary to libs// final File gdbServerFile = getAndroidNdk().getGdbServer( architecture ); final File destFile = new File( destDir, "gdbserver" ); if ( ! destFile.exists() ) { getLog().debug( "Copying gdbServer to " + destFile ); FileUtils.copyFile( gdbServerFile, destFile ); } else { getLog().info( "Note: gdbserver binary already exists at destination, will not copy over" ); } } } catch ( Exception e ) { getLog().error( "Error while copying gdbserver: " + e.getMessage(), e ); throw new MojoExecutionException( "Error while copying gdbserver: " + e.getMessage(), e ); } } private void copyLocalNativeLibraries( final File localNativeLibrariesDirectory, final File destinationDirectory ) throws MojoExecutionException { getLog().debug( "Copying existing native libraries from " + localNativeLibrariesDirectory ); try { IOFileFilter libSuffixFilter = FileFilterUtils.suffixFileFilter( ".so" ); IOFileFilter gdbserverNameFilter = FileFilterUtils.nameFileFilter( "gdbserver" ); IOFileFilter orFilter = FileFilterUtils.or( libSuffixFilter, gdbserverNameFilter ); IOFileFilter libFiles = FileFilterUtils.and( FileFileFilter.FILE, orFilter ); FileFilter filter = FileFilterUtils.or( DirectoryFileFilter.DIRECTORY, libFiles ); org.apache.commons.io.FileUtils .copyDirectory( localNativeLibrariesDirectory, destinationDirectory, filter ); } 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 generateIntermediateApk() throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger( this.getLog() ); File[] overlayDirectories = getResourceOverlayDirectories(); File androidJar = getAndroidSdk().getAndroidJar(); File outputFile = new File( targetDirectory, finalName + ".ap_" ); List dependencyArtifactResDirectoryList = new ArrayList(); for ( Artifact libraryArtifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) ) { final File libraryResDir = getUnpackedLibResourceFolder( libraryArtifact ); if ( libraryResDir.exists() ) { dependencyArtifactResDirectoryList.add( libraryResDir ); } } AaptCommandBuilder commandBuilder = AaptCommandBuilder .packageResources( getLog() ) .forceOverwriteExistingFiles() .setPathToAndroidManifest( destinationManifestFile ) .addResourceDirectoriesIfExists( overlayDirectories ) .addResourceDirectoryIfExists( resourceDirectory ) .addResourceDirectoriesIfExists( dependencyArtifactResDirectoryList ) .autoAddOverlay() // NB aapt only accepts a single assets parameter - combinedAssets is a merge of all assets .addRawAssetsDirectoryIfExists( combinedAssets ) .renameManifestPackage( renameManifestPackage ) .renameInstrumentationTargetPackage( renameInstrumentationTargetPackage ) .addExistingPackageToBaseIncludeSet( androidJar ) .setOutputApkFile( outputFile ) .addConfigurations( configurations ) .setVerbose( aaptVerbose ) .setDebugMode( !release ) .addExtraArguments( aaptExtraArgs ); getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() ); try { executor.setCaptureStdOut( true ); List commands = commandBuilder.build(); executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false ); } catch ( ExecutionException e ) { throw new MojoExecutionException( "", e ); } } protected AndroidSigner getAndroidSigner() { if ( sign == null ) { return new AndroidSigner( signDebug ); } else { return new AndroidSigner( sign.getDebug() ); } } /** * Used to populated the {@link #apkMetaInf} attribute via reflection. */ private MetaInf getDefaultMetaInf() { // check for deprecated first if ( apkMetaIncludes != null && apkMetaIncludes.length > 0 ) { return new MetaInf().include( apkMetaIncludes ); } return this.pluginMetaInf; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy