View Javadoc
1   /*
2    * Copyright (C) 2009 Jayway AB
3    * Copyright (C) 2007-2008 JVending Masa
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package com.simpligility.maven.plugins.android.phase09package;
18  
19  import com.android.sdklib.build.ApkBuilder;
20  import com.android.sdklib.build.ApkCreationException;
21  import com.android.sdklib.build.DuplicateFileException;
22  import com.android.sdklib.build.SealedApkException;
23  import com.google.common.io.Files;
24  import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
25  import com.simpligility.maven.plugins.android.AndroidNdk;
26  import com.simpligility.maven.plugins.android.AndroidSigner;
27  import com.simpligility.maven.plugins.android.CommandExecutor;
28  import com.simpligility.maven.plugins.android.ExecutionException;
29  import com.simpligility.maven.plugins.android.IncludeExcludeSet;
30  import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
31  import com.simpligility.maven.plugins.android.common.AndroidExtension;
32  import com.simpligility.maven.plugins.android.common.NativeHelper;
33  import com.simpligility.maven.plugins.android.config.ConfigHandler;
34  import com.simpligility.maven.plugins.android.config.ConfigPojo;
35  import com.simpligility.maven.plugins.android.config.PullParameter;
36  import com.simpligility.maven.plugins.android.configuration.Apk;
37  import com.simpligility.maven.plugins.android.configuration.MetaInf;
38  import com.simpligility.maven.plugins.android.configuration.Sign;
39  import org.apache.commons.io.FileUtils;
40  import org.apache.commons.io.filefilter.DirectoryFileFilter;
41  import org.apache.commons.io.filefilter.FileFileFilter;
42  import org.apache.commons.io.filefilter.FileFilterUtils;
43  import org.apache.commons.io.filefilter.IOFileFilter;
44  import org.apache.maven.artifact.Artifact;
45  import org.apache.maven.plugin.MojoExecutionException;
46  import org.apache.maven.plugin.MojoFailureException;
47  import org.apache.maven.plugins.annotations.LifecyclePhase;
48  import org.apache.maven.plugins.annotations.Mojo;
49  import org.apache.maven.plugins.annotations.Parameter;
50  import org.apache.maven.plugins.annotations.ResolutionScope;
51  import org.apache.maven.plugins.shade.resource.ResourceTransformer;
52  
53  import java.io.File;
54  import java.io.FileFilter;
55  import java.io.FileInputStream;
56  import java.io.FileNotFoundException;
57  import java.io.FileOutputStream;
58  import java.io.FilenameFilter;
59  import java.io.IOException;
60  import java.io.InputStream;
61  import java.io.OutputStream;
62  import java.util.ArrayList;
63  import java.util.Arrays;
64  import java.util.Collection;
65  import java.util.Enumeration;
66  import java.util.HashMap;
67  import java.util.HashSet;
68  import java.util.List;
69  import java.util.Map;
70  import java.util.Set;
71  import java.util.jar.JarOutputStream;
72  import java.util.regex.Matcher;
73  import java.util.regex.Pattern;
74  import java.util.zip.ZipEntry;
75  import java.util.zip.ZipFile;
76  import java.util.zip.ZipOutputStream;
77  
78  import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts;
79  import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
80  import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
81  import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;
82  
83  
84  
85  
86  /**
87   * Creates the apk file. By default signs it with debug keystore.<br>
88   * Change that by setting configuration parameter
89   * <code>&lt;sign&gt;&lt;debug&gt;false&lt;/debug&gt;&lt;/sign&gt;</code>.
90   *
91   * @author hugo.josefson@jayway.com
92   */
93  @Mojo( name = "apk",
94         defaultPhase = LifecyclePhase.PACKAGE,
95         requiresDependencyResolution = ResolutionScope.COMPILE )
96  public class ApkMojo extends AbstractAndroidMojo
97  {
98  
99      /**
100      * <p>How to sign the apk.</p>
101      * <p>Looks like this:</p>
102      * <pre>
103      * &lt;sign&gt;
104      *     &lt;debug&gt;auto&lt;/debug&gt;
105      * &lt;/sign&gt;
106      * </pre>
107      * <p>Valid values for <code>&lt;debug&gt;</code> are:
108      * <ul>
109      * <li><code>true</code> = sign with the debug keystore.
110      * <li><code>false</code> = don't sign with the debug keystore.
111      * <li><code>both</code> = create a signed as well as an unsigned apk.
112      * <li><code>auto</code> (default) = sign with debug keystore, unless another keystore is defined. (Signing with
113      * other keystores is not yet implemented. See
114      * <a href="http://code.google.com/p/maven-android-plugin/issues/detail?id=2">Issue 2</a>.)
115      * </ul></p>
116      * <p>Can also be configured from command-line with parameter <code>-Dandroid.sign.debug</code>.</p>
117      */
118     @Parameter
119     private Sign sign;
120 
121     /**
122      * <p>Parameter designed to pick up <code>-Dandroid.sign.debug</code> in case there is no pom with a
123      * <code>&lt;sign&gt;</code> configuration tag.</p>
124      * <p>Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Sign#debug}.</p>
125      */
126     @Parameter( property = "android.sign.debug", defaultValue = "auto", readonly = true )
127     private String signDebug;
128 
129     /**
130      * <p>Rewrite the manifest so that all of its instrumentation components target the given package.
131      * This value will be passed on to the aapt parameter --rename-instrumentation-target-package.
132      * Look to aapt for more help on this. </p>
133      *
134      * TODO pass this into AaptExecutor
135      */
136     @Parameter( property = "android.renameInstrumentationTargetPackage" )
137     private String renameInstrumentationTargetPackage;
138 
139     /**
140      * <p>Allows to detect and extract the duplicate files from embedded jars. In that case, the plugin analyzes
141      * the content of all embedded dependencies and checks they are no duplicates inside those dependencies. Indeed,
142      * Android does not support duplicates, and all dependencies are inlined in the APK. If duplicates files are found,
143      * the resource is kept in the first dependency and removes from others.
144      */
145     @Parameter( property = "android.extractDuplicates", defaultValue = "false" )
146     private boolean extractDuplicates;
147 
148     /**
149      * <p>Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.</p>
150      */
151     @Parameter
152     private String classifier;
153 
154     /**
155      * The apk file produced by the apk goal. Per default the file is placed into the build directory (target
156      * normally) using the build final name and apk as extension.
157      */
158     @Parameter( property = "android.outputApk",
159                 defaultValue = "${project.build.directory}/${project.build.finalName}.apk" )
160     private String outputApk;
161 
162     /**
163      * <p>Additional source directories that contain resources to be packaged into the apk.</p>
164      * <p>These are not source directories, that contain java classes to be compiled.
165      * It corresponds to the -df option of the apkbuilder program. It allows you to specify directories,
166      * that contain additional resources to be packaged into the apk. </p>
167      * So an example inside the plugin configuration could be:
168      * <pre>
169      * &lt;configuration&gt;
170      *   ...
171      *    &lt;sourceDirectories&gt;
172      *      &lt;sourceDirectory&gt;${project.basedir}/additionals&lt;/sourceDirectory&gt;
173      *   &lt;/sourceDirectories&gt;
174      *   ...
175      * &lt;/configuration&gt;
176      * </pre>
177      */
178     @Parameter( property = "android.sourceDirectories" )
179     private File[] sourceDirectories;
180 
181     /**
182      * Pattern for additional META-INF resources to be packaged into the apk.
183      * <p>
184      * The APK builder filters these resources and doesn't include them into the apk.
185      * This leads to bad behaviour of dependent libraries relying on these resources,
186      * for instance service discovery doesn't work.<br>
187      * By specifying this pattern, the android plugin adds these resources to the final apk.
188      * </p>
189      * <p>The pattern is relative to META-INF, i.e. one must use
190      * <pre>
191      * <code>
192      * &lt;apkMetaIncludes&gt;
193      *     &lt;metaInclude>services/**&lt;/metaInclude&gt;
194      * &lt;/apkMetaIncludes&gt;
195      * </code>
196      * </pre>
197      * ... instead of
198      * <pre>
199      * <code>
200      * &lt;apkMetaIncludes&gt;
201      *     &lt;metaInclude>META-INF/services/**&lt;/metaInclude&gt;
202      * &lt;/apkMetaIncludes&gt;
203      * </code>
204      * </pre>
205      * <p>
206      * See also <a href="http://code.google.com/p/maven-android-plugin/issues/detail?id=97">Issue 97</a>
207      * </p>
208      *
209      * @deprecated in favour of apk.metaInf
210      */
211     @PullParameter
212     private String[] apkMetaIncludes;
213 
214     @PullParameter( defaultValueGetterMethod = "getDefaultMetaInf" )
215     private MetaInf apkMetaInf;
216 
217     @Parameter( alias = "metaInf" )
218     private MetaInf pluginMetaInf;
219 
220     /**
221      * Defines whether or not the APK is being produced in debug mode or not.
222      */
223     @Parameter( property = "android.apk.debug" )
224     @PullParameter( defaultValue = "false" )
225     private Boolean apkDebug;
226 
227     @Parameter( property = "android.nativeToolchain" )
228     @PullParameter( defaultValue = "arm-linux-androideabi-4.4.3" )
229     private String apkNativeToolchain;
230 
231     /**
232      * Specifies the final name of the library output by the build (this allows
233      */
234     @Parameter( property = "android.ndk.build.build.final-library.name" )
235     private String ndkFinalLibraryName;
236 
237     /**
238      * Specify a list of patterns that are matched against the names of jar file
239      * dependencies. Matching jar files will not have their resources added to the
240      * resulting APK.
241      *
242      * The patterns are standard Java regexes.
243      */
244     @Parameter
245     private String[] excludeJarResources;
246 
247     private Pattern[] excludeJarResourcesPatterns;
248 
249     /**
250      * Embedded configuration of this mojo.
251      */
252     @Parameter
253     @ConfigPojo( prefix = "apk" )
254     private Apk apk;
255 
256     /**
257      * Skips transitive dependencies. May be useful if the target classes directory is populated with the
258      * {@code maven-dependency-plugin} and already contains all dependency classes.
259      */
260     @Parameter( property = "skipDependencies", defaultValue = "false" )
261     private boolean skipDependencies;
262 
263     /**
264      * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the
265      * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty
266      * strings are ignored. Example:
267      * <pre>
268      *     &lt;artifactTypeSet&gt;
269      *         &lt;includes&gt;
270      *             &lt;include&gt;aar&lt;/include&gt;
271      *         &lt;includes&gt;
272      *         &lt;excludes&gt;
273      *             &lt;exclude&gt;jar&lt;/exclude&gt;
274      *         &lt;excludes&gt;
275      *     &lt;/artifactTypeSet&gt;
276      * </pre>
277      */
278     @Parameter( property = "artifactTypeSet" )
279     private IncludeExcludeSet artifactTypeSet;
280 
281     /**
282      * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The
283      * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can
284      * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId},
285      * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using
286      * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact
287      * range. Empty strings are ignored. Example:
288      * <pre>
289      *     &lt;artifactTypeSet&gt;
290      *         &lt;includes&gt;
291      *             &lt;include&gt;foo-group:foo-artifact:1.0-SNAPSHOT&lt;/include&gt;
292      *             &lt;include&gt;bar-group:bar-artifact:1.0-SNAPSHOT&lt;/include&gt;
293      *             &lt;include&gt;baz-group:*&lt;/include&gt;
294      *         &lt;includes&gt;
295      *         &lt;excludes&gt;
296      *             &lt;exclude&gt;qux-group:qux-artifact:*&lt;/exclude&gt;
297      *         &lt;excludes&gt;
298      *     &lt;/artifactTypeSet&gt;
299      * </pre>
300      */
301     @Parameter( property = "artifactSet" )
302     private IncludeExcludeSet artifactSet;
303 
304     private static final Pattern PATTERN_JAR_EXT = Pattern.compile( "^.+\\.jar$", Pattern.CASE_INSENSITIVE );
305 
306     private static final String DEX_SUFFIX = ".dex";
307 
308     private static final String CLASSES = "classes";
309 
310     /**
311      * <p>Default hardware architecture for native library dependencies (with {@code &lt;type>so&lt;/type>})
312      * without a classifier.</p>
313      * <p>Valid values currently include {@code armeabi}, {@code armeabi-v7a}, {@code mips} and {@code x86}.</p>
314      */
315     @Parameter( property = "android.nativeLibrariesDependenciesHardwareArchitectureDefault", defaultValue = "armeabi" )
316     private String nativeLibrariesDependenciesHardwareArchitectureDefault;
317 
318     @Parameter
319     private ResourceTransformer[] transformers;
320 
321     /**
322      * @throws MojoExecutionException
323      * @throws MojoFailureException
324      */
325     public void execute() throws MojoExecutionException, MojoFailureException
326     {
327 
328         // Make an early exit if we're not supposed to generate the APK
329         if ( ! generateApk )
330         {
331             return;
332         }
333 
334         ConfigHandler cfh = new ConfigHandler( this, this.session, this.execution );
335 
336         cfh.parseConfiguration();
337 
338         generateIntermediateApk();
339 
340         // Compile resource exclusion patterns, if any
341         if ( excludeJarResources != null && excludeJarResources.length > 0 )
342         {
343           getLog().debug( "Compiling " + excludeJarResources.length + " patterns" );
344 
345           excludeJarResourcesPatterns = new Pattern[excludeJarResources.length];
346 
347           for ( int index = 0; index < excludeJarResources.length; ++index )
348           {
349             excludeJarResourcesPatterns[index] = Pattern.compile( excludeJarResources[index] );
350           }
351         }
352 
353         // Initialize apk build configuration
354         File outputFile = new File( outputApk );
355         final boolean signWithDebugKeyStore = getAndroidSigner().isSignWithDebugKeyStore();
356 
357         if ( getAndroidSigner().shouldCreateBothSignedAndUnsignedApk() )
358         {
359             getLog().info( "Creating debug key signed apk file " + outputFile );
360             createApkFile( outputFile, true );
361             final File unsignedOutputFile = new File( targetDirectory,
362                     finalName + "-unsigned." + APK );
363             getLog().info( "Creating additional unsigned apk file " + unsignedOutputFile );
364             createApkFile( unsignedOutputFile, false );
365             projectHelper.attachArtifact( project, unsignedOutputFile,
366                     classifier == null ? "unsigned" : classifier + "_unsigned" );
367         }
368         else
369         {
370             createApkFile( outputFile, signWithDebugKeyStore );
371         }
372 
373         if ( classifier == null )
374         {
375             // Set the generated .apk file as the main artifact (because the pom states <packaging>apk</packaging>)
376             project.getArtifact().setFile( outputFile );
377         }
378         else
379         {
380             // If there is a classifier specified, attach the artifact using that
381             projectHelper.attachArtifact( project, AndroidExtension.APK, classifier, outputFile );
382         }
383     }
384 
385     void createApkFile( File outputFile, boolean signWithDebugKeyStore ) throws MojoExecutionException
386     {
387         //this needs to come from DexMojo
388         File dexFile = new File( targetDirectory, "classes.dex" );
389         if ( !dexFile.exists() )
390         {
391             dexFile = new File( targetDirectory, "classes.zip" );
392         }
393 
394         File zipArchive = new File( targetDirectory, finalName + ".ap_" );
395         ArrayList<File> sourceFolders = new ArrayList<File>();
396         if ( sourceDirectories != null )
397         {
398             sourceFolders.addAll( Arrays.asList( sourceDirectories ) );
399         }
400         ArrayList<File> jarFiles = new ArrayList<File>();
401 
402         // Process the native libraries, looking both in the current build directory as well as
403         // at the dependencies declared in the pom.  Currently, all .so files are automatically included
404         final Collection<File> nativeFolders = getNativeLibraryFolders();
405         getLog().info( "Adding native libraries : " + nativeFolders );
406 
407         doAPKWithAPKBuilder( outputFile, dexFile, zipArchive, sourceFolders, jarFiles, nativeFolders,
408                 signWithDebugKeyStore );
409         
410         if ( this.apkMetaInf != null )
411         {
412             File outputJar = new File( outputApk.substring( 0, outputApk.length() - 3 ) + "jar" );
413             if ( outputJar.exists() ) 
414             {
415                 jarFiles.add( outputJar );
416             } 
417             else 
418             {
419                 getLog().warn( "Output jar doesn't exist:" + outputJar );
420             }
421             try
422             {
423                 addMetaInf( outputFile, jarFiles );
424             }
425             catch ( IOException e )
426             {
427                 throw new MojoExecutionException( "Could not add META-INF resources.", e );
428             }
429         }
430     }
431 
432     private void addMetaInf( File outputFile, ArrayList<File> jarFiles ) throws IOException
433     {
434         File tmp = File.createTempFile( outputFile.getName(), ".add", outputFile.getParentFile() );
435 
436         FileOutputStream fos = new FileOutputStream( tmp );
437         JarOutputStream zos = new JarOutputStream( fos );
438         Set<String> entries = new HashSet<String>();
439 
440         updateWithMetaInf( zos, outputFile, entries, false );
441 
442         for ( File f : jarFiles )
443         {
444             updateWithMetaInf( zos, f, entries, true );
445         }
446 
447         if ( transformers != null )
448         {
449             for ( ResourceTransformer transformer : transformers )
450             {
451                 if ( transformer.hasTransformedResource() )
452                 {
453                     transformer.modifyOutputStream( zos );
454                 }
455             }
456         }
457 
458         zos.close();
459 
460         outputFile.delete();
461 
462         if ( ! tmp.renameTo( outputFile ) )
463         {
464             throw new IOException( String.format( "Cannot rename %s to %s", tmp, outputFile.getName() ) );
465         }
466     }
467 
468     private void updateWithMetaInf( ZipOutputStream zos, File jarFile, Set<String> entries, boolean metaInfOnly )
469             throws IOException
470     {
471         ZipFile zin = new ZipFile( jarFile );
472 
473         for ( Enumeration<? extends ZipEntry> en = zin.entries(); en.hasMoreElements(); )
474         {
475             ZipEntry ze = en.nextElement();
476 
477             if ( ze.isDirectory() )
478             {
479                 continue;
480             }
481 
482             String zn = ze.getName();
483 
484             if ( metaInfOnly )
485             {
486                 if ( ! zn.startsWith( "META-INF/" ) )
487                 {
488                     continue;
489                 }
490 
491                 if ( ! this.apkMetaInf.isIncluded( zn ) )
492                 {
493                     continue;
494                 }
495             }
496 
497 
498 
499             boolean resourceTransformed = false;
500 
501             if ( transformers != null )
502             {
503                 for ( ResourceTransformer transformer : transformers )
504                 {
505                     if ( transformer.canTransformResource( zn ) )
506                     {
507                         getLog().info( "Transforming " + zn + " using " + transformer.getClass().getName() );
508                         InputStream is = zin.getInputStream( ze );
509                         transformer.processResource( zn, is, null );
510                         is.close();
511                         resourceTransformed = true;
512                         break;
513                     }
514                 }
515             }
516 
517             if ( !resourceTransformed )
518             {
519                 // Avoid duplicates that aren't accounted for by the resource transformers
520                 if ( metaInfOnly && this.extractDuplicates && ! entries.add( zn ) )
521                 {
522                     continue;
523                 }
524 
525                 InputStream is = zin.getInputStream( ze );
526 
527                 final ZipEntry ne;
528                 if ( ze.getMethod() == ZipEntry.STORED )
529                 {
530                     ne = new ZipEntry( ze );
531                 }
532                 else
533                 {
534                     ne = new ZipEntry( zn );
535                 }
536 
537                 zos.putNextEntry( ne );
538 
539                 copyStreamWithoutClosing( is, zos );
540 
541                 is.close();
542                 zos.closeEntry();
543             }
544         }
545 
546         zin.close();
547     }
548 
549     private Map<String, List<File>> jars = new HashMap<String, List<File>>();
550 
551     private void computeDuplicateFiles( File jar ) throws IOException
552     {
553         ZipFile file = new ZipFile( jar );
554         Enumeration<? extends ZipEntry> list = file.entries();
555         while ( list.hasMoreElements() )
556         {
557             ZipEntry ze = list.nextElement();
558             if ( ! ( ze.getName().contains( "META-INF/" ) || ze.isDirectory() ) )
559             { // Exclude META-INF and Directories
560                 List<File> l = jars.get( ze.getName() );
561                 if ( l == null )
562                 {
563                     l = new ArrayList<File>();
564                     jars.put( ze.getName(), l );
565                 }
566                 l.add( jar );
567             }
568         }
569     }
570 
571     private void computeDuplicateFilesInSource( File folder )
572     {
573         String rPath = folder.getAbsolutePath();
574         for ( File file : Files.fileTreeTraverser().breadthFirstTraversal( folder ).toList() )
575         {
576             String lPath = file.getAbsolutePath();
577             if ( lPath.equals( rPath ) )
578             {
579                 continue; //skip the root
580             }
581             lPath = lPath.substring( rPath.length() + 1 ); //strip root folder to make relative path
582 
583             if ( jars.get( lPath ) == null )
584             {
585                 jars.put( lPath, new ArrayList<File>() );
586             }
587             jars.get( lPath ).add( folder );
588         }
589     }
590 
591     private void extractDuplicateFiles( List<File> jarFiles, Collection<File> sourceFolders ) throws IOException
592     {
593         getLog().debug( "Extracting duplicates" );
594         List<String> duplicates = new ArrayList<String>();
595         List<File> jarToModify = new ArrayList<File>();
596         for ( String s : jars.keySet() )
597         {
598             List<File> l = jars.get( s );
599             if ( l.size() > 1 )
600             {
601                 getLog().warn( "Duplicate file " + s + " : " + l );
602                 duplicates.add( s );
603                 for ( int i = 0; i < l.size(); i++ )
604                 {
605                     if ( ! jarToModify.contains( l.get( i ) ) )
606                     {
607                         jarToModify.add( l.get( i ) );
608                     }
609                 }
610             }
611         }
612 
613         // Rebuild jars.  Remove duplicates from ALL jars, then add them back into a duplicate-resources.jar
614         File tmp = new File( targetDirectory.getAbsolutePath(), "unpacked-embedded-jars" );
615         tmp.mkdirs();
616         File duplicatesJar = new File( tmp, "duplicate-resources.jar" );
617         Set<String> duplicatesAdded = new HashSet<String>();
618 
619         duplicatesJar.createNewFile();
620         final FileOutputStream fos = new FileOutputStream( duplicatesJar );
621         final JarOutputStream zos = new JarOutputStream( fos );
622 
623         for ( File file : jarToModify )
624         {
625             final int index = jarFiles.indexOf( file );
626             if ( index != -1 )
627             {
628                 final File newJar = removeDuplicatesFromJar( file, duplicates, duplicatesAdded, zos, index );
629                 getLog().debug( "Removed duplicates from " + newJar );
630                 if ( newJar != null )
631                 {
632                     jarFiles.set( index, newJar );
633                 }
634             }
635             else
636             {
637                 removeDuplicatesFromFolder( file, file, duplicates, duplicatesAdded, zos );
638                 getLog().debug( "Removed duplicates from " + file );
639             }
640         }
641         //add transformed resources to duplicate-resources.jar
642         if ( transformers != null )
643         {
644             for ( ResourceTransformer transformer : transformers )
645             {
646                 if ( transformer.hasTransformedResource() )
647                 {
648                     transformer.modifyOutputStream( zos );
649                 }
650             }
651         }
652         zos.close();
653         fos.close();
654 
655         if ( !jarToModify.isEmpty() && duplicatesJar.length() > 0 )
656         {
657             jarFiles.add( duplicatesJar );
658         }
659     }
660 
661     /**
662      * Creates the APK file using the internal APKBuilder.
663      *
664      * @param outputFile            the output file
665      * @param dexFile               the dex file
666      * @param zipArchive            the classes folder
667      * @param sourceFolders         the resources
668      * @param jarFiles              the embedded java files
669      * @param nativeFolders         the native folders
670      * @param signWithDebugKeyStore enables the signature of the APK using the debug key
671      * @throws MojoExecutionException if the APK cannot be created.
672      */
673     private void doAPKWithAPKBuilder( File outputFile, File dexFile, File zipArchive, Collection<File> sourceFolders,
674                                       List<File> jarFiles, Collection<File> nativeFolders,
675                                       boolean signWithDebugKeyStore ) throws MojoExecutionException
676     {
677         getLog().debug( "Building APK with internal APKBuilder" );
678         
679         //A when jack is running the classes directory will not get filled (usually)
680         // so let's skip it if it wasn't created by something else
681         if ( projectOutputDirectory.exists() || !getJack().isEnabled() ) 
682         {
683             sourceFolders.add( projectOutputDirectory );
684         }
685 
686         for ( Artifact artifact : filterArtifacts( getRelevantCompileArtifacts(), skipDependencies,
687                 artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
688                 artifactSet.getExcludes() ) )
689         {
690             getLog().debug( "Found artifact for APK :" + artifact );
691             if ( extractDuplicates )
692             {
693                 try
694                 {
695                     computeDuplicateFiles( artifact.getFile() );
696                 }
697                 catch ( Exception e )
698                 {
699                     getLog().warn( "Cannot compute duplicates files from " + artifact.getFile().getAbsolutePath(), e );
700                 }
701             }
702             jarFiles.add( artifact.getFile() );
703         }
704 
705         for ( File src : sourceFolders )
706         {
707             computeDuplicateFilesInSource( src );
708         }
709 
710         // Check duplicates.
711         if ( extractDuplicates )
712         {
713             try
714             {
715                 extractDuplicateFiles( jarFiles, sourceFolders );
716             }
717             catch ( IOException e )
718             {
719                 getLog().error( "Could not extract duplicates to duplicate-resources.jar", e );
720             }
721         }
722 
723         try
724         {
725             final String debugKeyStore = signWithDebugKeyStore ? ApkBuilder.getDebugKeystore() : null;
726             final ApkBuilder apkBuilder = new ApkBuilder( outputFile, zipArchive, dexFile, debugKeyStore, null );
727             if ( apkDebug )
728             {
729                 apkBuilder.setDebugMode( true );
730             }
731 
732             for ( File sourceFolder : sourceFolders )
733             {
734                 getLog().debug( "Adding source folder : " + sourceFolder );
735                 // Use ApkBuilder#addFile() to explicitly add resource files so that we can add META-INF/services.
736                 addResourcesFromFolder( apkBuilder, sourceFolder );
737             }
738 
739             for ( File jarFile : jarFiles )
740             {
741                 boolean excluded = false;
742 
743                 if ( excludeJarResourcesPatterns != null )
744                 {
745                     final String name = jarFile.getName();
746                     getLog().debug( "Checking " + name + " against patterns" );
747                     for ( Pattern pattern : excludeJarResourcesPatterns )
748                     {
749                         final Matcher matcher = pattern.matcher( name );
750                         if ( matcher.matches() )
751                         {
752                             getLog().debug( "Jar " + name + " excluded by pattern " + pattern );
753                             excluded = true;
754                             break;
755                         }
756                         else
757                         {
758                             getLog().debug( "Jar " + name + " not excluded by pattern " + pattern );
759                         }
760                     }
761                 }
762 
763                 if ( excluded )
764                 {
765                     continue;
766                 }
767 
768                 if ( jarFile.isDirectory() )
769                 {
770                     getLog().debug( "Adding resources from jar folder : " + jarFile );
771                     final String[] filenames = jarFile.list( new FilenameFilter()
772                     {
773                         public boolean accept( File dir, String name )
774                         {
775                             return PATTERN_JAR_EXT.matcher( name ).matches();
776                         }
777                     } );
778 
779                     for ( String filename : filenames )
780                     {
781                         final File innerJar = new File( jarFile, filename );
782                         getLog().debug( "Adding resources from innerJar : " + innerJar );
783                         apkBuilder.addResourcesFromJar( innerJar );
784                     }
785                 }
786                 else
787                 {
788                     getLog().debug( "Adding resources from : " + jarFile );
789                     apkBuilder.addResourcesFromJar( jarFile );
790                 }
791             }
792 
793             addSecondaryDexes( dexFile, apkBuilder );
794 
795             for ( File nativeFolder : nativeFolders )
796             {
797                 getLog().debug( "Adding native library : " + nativeFolder );
798                 apkBuilder.addNativeLibraries( nativeFolder );
799             }
800             apkBuilder.sealApk();
801         }
802         catch ( ApkCreationException | SealedApkException | IOException e )
803         {
804             throw new MojoExecutionException( e.getMessage(), e );
805         }
806         catch ( DuplicateFileException e )
807         {
808             final String msg = String.format( "Duplicated file: %s, found in archive %s and %s",
809                     e.getArchivePath(), e.getFile1(), e.getFile2() );
810             throw new MojoExecutionException( msg, e );
811         }
812     }
813 
814     /**
815      * Collect all Files from Folder (recursively) that are not class files.
816      */
817     private void collectFiles( File folder, final List<File> collectedFiles )
818     {
819         folder.listFiles( new FileFilter()
820         {
821             @Override
822             public boolean accept( File file )
823             {
824                 if ( file.isDirectory() )
825                 {
826                     collectFiles( file, collectedFiles );
827                 }
828                 else if ( file.isFile() )
829                 {
830                     if ( !file.getName().endsWith( ".class" ) )
831                     {
832                         collectedFiles.add( file );
833                     }
834                 }
835                 return false;
836             }
837         } );
838 
839     }
840     /**
841      * Adds all non-class files from folder, so that we can add META-INF/services resources.
842      */
843     private void addResourcesFromFolder( ApkBuilder builder,  File folder )
844             throws SealedApkException, DuplicateFileException, ApkCreationException, IOException
845     {
846         final int folderPathLength = folder.getCanonicalPath().length();
847 
848         final List<File> resourceFiles = new ArrayList<>(  );
849         collectFiles( folder, resourceFiles );
850 
851         for ( final File resourceFile : resourceFiles )
852         {
853             final String resourceName = resourceFile
854                     .getCanonicalPath()
855                     .substring( folderPathLength + 1 )
856                     .replaceAll( "\\\\", "/" );
857             getLog().info( "Adding resource " + resourceFile + " : " + resourceName );
858             builder.addFile( resourceFile,  resourceName );
859         }
860     }
861 
862     private void addSecondaryDexes( File dexFile, ApkBuilder apkBuilder ) throws ApkCreationException,
863             SealedApkException, DuplicateFileException
864     {
865         int dexNumber = 2;
866         String dexFileName = getNextDexFileName( dexNumber );
867         File secondDexFile = createNextDexFile( dexFile, dexFileName );
868         while ( secondDexFile.exists() )
869         {
870             apkBuilder.addFile( secondDexFile, dexFileName );
871             dexNumber++;
872             dexFileName = getNextDexFileName( dexNumber );
873             secondDexFile = createNextDexFile( dexFile, dexFileName );
874         }
875     }
876 
877     private File createNextDexFile( File dexFile, String dexFileName )
878     {
879         return new File( dexFile.getParentFile(), dexFileName );
880     }
881 
882     private String getNextDexFileName( int dexNumber )
883     {
884         return CLASSES + dexNumber + DEX_SUFFIX;
885     }
886 
887     private File removeDuplicatesFromJar( File in, List<String> duplicates,
888                                           Set<String> duplicatesAdded, ZipOutputStream duplicateZos, int num )
889     {
890         String target = targetDirectory.getAbsolutePath();
891         File tmp = new File( target, "unpacked-embedded-jars" );
892         tmp.mkdirs();
893         String jarName = String.format( "%s-%d.%s",
894            Files.getNameWithoutExtension( in.getName() ), num, Files.getFileExtension( in.getName() ) );
895         File out = new File( tmp, jarName );
896 
897         if ( out.exists() )
898         {
899             return out;
900         }
901         else
902         {
903             try
904             {
905                 out.createNewFile();
906             }
907             catch ( IOException e )
908             {
909                 e.printStackTrace();
910             }
911         }
912 
913         // Create a new Jar file
914         final FileOutputStream fos;
915         final ZipOutputStream jos;
916         try
917         {
918             fos = new FileOutputStream( out );
919             jos = new ZipOutputStream( fos );
920         }
921         catch ( FileNotFoundException e1 )
922         {
923             getLog().error( "Cannot remove duplicates : the output file " + out.getAbsolutePath() + " does not found" );
924             return null;
925         }
926 
927         final ZipFile inZip;
928         try
929         {
930             inZip = new ZipFile( in );
931             Enumeration<? extends ZipEntry> entries = inZip.entries();
932             while ( entries.hasMoreElements() )
933             {
934                 ZipEntry entry = entries.nextElement();
935                 // If the entry is not a duplicate, copy.
936                 if ( ! duplicates.contains( entry.getName() ) )
937                 {
938                     // copy the entry header to jos
939                     jos.putNextEntry( entry );
940                     InputStream currIn = inZip.getInputStream( entry );
941                     copyStreamWithoutClosing( currIn, jos );
942                     currIn.close();
943                     jos.closeEntry();
944                 }
945                 //if it is duplicate, check the resource transformers
946                 else
947                 {
948                     boolean resourceTransformed = false;
949                     if ( transformers != null )
950                     {
951                         for ( ResourceTransformer transformer : transformers )
952                         {
953                             if ( transformer.canTransformResource( entry.getName() ) )
954                             {
955                                 getLog().info( "Transforming " + entry.getName()
956                                         + " using " + transformer.getClass().getName() );
957                                 InputStream currIn = inZip.getInputStream( entry );
958                                 transformer.processResource( entry.getName(), currIn, null );
959                                 currIn.close();
960                                 resourceTransformed = true;
961                                 break;
962                             }
963                         }
964                     }
965                     //if not handled by transformer, add (once) to duplicates jar
966                     if ( !resourceTransformed )
967                     {
968                         if ( !duplicatesAdded.contains( entry.getName() ) )
969                         {
970                             duplicatesAdded.add( entry.getName() );
971                             duplicateZos.putNextEntry( entry );
972                             InputStream currIn = inZip.getInputStream( entry );
973                             copyStreamWithoutClosing( currIn, duplicateZos );
974                             currIn.close();
975                             duplicateZos.closeEntry();
976                         }
977                     }
978                 }
979             }
980         }
981         catch ( IOException e )
982         {
983             getLog().error( "Cannot removing duplicates : " + e.getMessage() );
984             return null;
985         }
986 
987         try
988         {
989             inZip.close();
990             jos.close();
991             fos.close();
992         }
993         catch ( IOException e )
994         {
995             // ignore it.
996         }
997         getLog().info( in.getName() + " rewritten without duplicates : " + out.getAbsolutePath() );
998         return out;
999     }
1000 
1001     private void removeDuplicatesFromFolder( File root, File in, List<String> duplicates,
1002        Set<String> duplicatesAdded, ZipOutputStream duplicateZos )
1003     {
1004         String rPath = root.getAbsolutePath();
1005         try
1006         {
1007             for ( File f : in.listFiles() )
1008             {
1009                 if ( f.isDirectory() )
1010                 {
1011                     removeDuplicatesFromFolder( root, f, duplicates, duplicatesAdded, duplicateZos );
1012                 }
1013                 else
1014                 {
1015                     String lName = f.getAbsolutePath();
1016                     lName = lName.substring( rPath.length() + 1 ); //make relative path
1017                     if ( duplicates.contains( lName ) )
1018                     {
1019                         boolean resourceTransformed = false;
1020                         if ( transformers != null )
1021                         {
1022                             for ( ResourceTransformer transformer : transformers )
1023                             {
1024                                 if ( transformer.canTransformResource( lName ) )
1025                                 {
1026                                     getLog().info( "Transforming " + lName
1027                                        + " using " + transformer.getClass().getName() );
1028                                     InputStream currIn = new FileInputStream( f );
1029                                     transformer.processResource( lName, currIn, null );
1030                                     currIn.close();
1031                                     resourceTransformed = true;
1032                                     break;
1033                                 }
1034                             }
1035                         }
1036                         //if not handled by transformer, add (once) to duplicates jar
1037                         if ( !resourceTransformed )
1038                         {
1039                             if ( !duplicatesAdded.contains( lName ) )
1040                             {
1041                                 duplicatesAdded.add( lName );
1042                                 ZipEntry entry = new ZipEntry( lName );
1043                                 duplicateZos.putNextEntry( entry );
1044                                 InputStream currIn = new FileInputStream( f );
1045                                 copyStreamWithoutClosing( currIn, duplicateZos );
1046                                 currIn.close();
1047                                 duplicateZos.closeEntry();
1048                             }
1049                         }
1050                         f.delete();
1051                     }
1052                 }
1053             }
1054         }
1055         catch ( IOException e )
1056         {
1057             getLog().error( "Cannot removing duplicates : " + e.getMessage() );
1058         }
1059     }
1060 
1061     /**
1062      * Copies an input stream into an output stream but does not close the streams.
1063      *
1064      * @param in  the input stream
1065      * @param out the output stream
1066      * @throws IOException if the stream cannot be copied
1067      */
1068     private static void copyStreamWithoutClosing( InputStream in, OutputStream out ) throws IOException
1069     {
1070         final int bufferSize = 4096;
1071         byte[] b = new byte[ bufferSize ];
1072         int n;
1073         while ( ( n = in.read( b ) ) != - 1 )
1074         {
1075             out.write( b, 0, n );
1076         }
1077     }
1078 
1079     private Collection<File> getNativeLibraryFolders() throws MojoExecutionException
1080     {
1081         final List<File> natives = new ArrayList<File>();
1082 
1083         if ( nativeLibrariesDirectory.exists() )
1084         {
1085             // If we have prebuilt native libs then copy them over to the native output folder.
1086             // NB they will be copied over the top of any native libs generated as part of the NdkBuildMojo
1087             copyLocalNativeLibraries( nativeLibrariesDirectory, ndkOutputDirectory );
1088         }
1089 
1090         final Set<Artifact> artifacts = getNativeLibraryArtifacts();
1091         for ( Artifact resolvedArtifact : artifacts )
1092         {
1093             if ( APKLIB.equals( resolvedArtifact.getType() ) || AAR.equals( resolvedArtifact.getType() ) )
1094             {
1095                 // If the artifact is an AAR or APKLIB then add their native libs folder to the result.
1096                 final File folder = getUnpackedLibNativesFolder( resolvedArtifact );
1097                 getLog().debug( "Adding native library folder " + folder );
1098                 natives.add( folder );
1099             }
1100 
1101             // Copy the native lib dependencies into the native lib output folder
1102             for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES )
1103             {
1104                 if ( NativeHelper.artifactHasHardwareArchitecture( resolvedArtifact,
1105                         ndkArchitecture, nativeLibrariesDependenciesHardwareArchitectureDefault ) )
1106                 {
1107                     // If the artifact is a native lib then copy it into the native libs output folder.
1108                     copyNativeLibraryArtifact( resolvedArtifact, ndkOutputDirectory, ndkArchitecture );
1109                 }
1110             }
1111         }
1112 
1113         if ( apkDebug )
1114         {
1115             // Copy the gdbserver binary into the native libs output folder (for each architecture).
1116             for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES )
1117             {
1118                 copyGdbServer( ndkOutputDirectory, ndkArchitecture );
1119             }
1120         }
1121 
1122         if ( ndkOutputDirectory.exists() )
1123         {
1124             // If we have any native libs in the native output folder then add the output folder to the result.
1125             getLog().debug( "Adding built native library folder " + ndkOutputDirectory );
1126             natives.add( ndkOutputDirectory );
1127         }
1128 
1129         return natives;
1130     }
1131 
1132     /**
1133      * @return Any native dependencies or attached artifacts. This may include artifacts from the ndk-build MOJO.
1134      * @throws MojoExecutionException
1135      */
1136     private Set<Artifact> getNativeLibraryArtifacts() throws MojoExecutionException
1137     {
1138         return getNativeHelper().getNativeDependenciesArtifacts( this, getUnpackedLibsDirectory(), true );
1139     }
1140 
1141     private void copyNativeLibraryArtifact( Artifact artifact,
1142                                             File destinationDirectory,
1143                                             String ndkArchitecture ) throws MojoExecutionException
1144     {
1145 
1146         final File artifactFile = getArtifactResolverHelper().resolveArtifactToFile( artifact );
1147         try
1148         {
1149             final String artifactId = artifact.getArtifactId();
1150             String filename = artifactId.startsWith( "lib" )
1151                     ? artifactId + ".so"
1152                     : "lib" + artifactId + ".so";
1153             if ( ndkFinalLibraryName != null
1154                     && artifact.getFile().getName().startsWith( "lib" + ndkFinalLibraryName ) )
1155             {
1156                 // The artifact looks like one we built with the NDK in this module
1157                 // preserve the name from the NDK build
1158                 filename = artifact.getFile().getName();
1159             }
1160 
1161             final File folder = new File( destinationDirectory, ndkArchitecture );
1162             final File file = new File( folder, filename );
1163             getLog().debug( "Copying native dependency " + artifactId + " (" + artifact.getGroupId() + ") to " + file );
1164             FileUtils.copyFile( artifactFile, file );
1165         }
1166         catch ( IOException e )
1167         {
1168             throw new MojoExecutionException( "Could not copy native dependency.", e );
1169         }
1170     }
1171 
1172 
1173     /**
1174      * Copy the Ndk GdbServer into the architecture output folder if the folder exists but the GdbServer doesn't.
1175      */
1176     private void copyGdbServer( File destinationDirectory, String architecture ) throws MojoExecutionException
1177     {
1178 
1179         try
1180         {
1181             final File destDir = new File( destinationDirectory, architecture );
1182             if ( destDir.exists() )
1183             {
1184                 // Copy the gdbserver binary to libs/<architecture>/
1185                 final File gdbServerFile = getAndroidNdk().getGdbServer( architecture );
1186                 final File destFile = new File( destDir, "gdbserver" );
1187                 if ( ! destFile.exists() )
1188                 {
1189                     getLog().debug( "Copying gdbServer to " + destFile );
1190                     FileUtils.copyFile( gdbServerFile, destFile );
1191                 }
1192                 else
1193                 {
1194                     getLog().info( "Note: gdbserver binary already exists at destination, will not copy over" );
1195                 }
1196             }
1197         }
1198         catch ( Exception e )
1199         {
1200             getLog().error( "Error while copying gdbserver: " + e.getMessage(), e );
1201             throw new MojoExecutionException( "Error while copying gdbserver: " + e.getMessage(), e );
1202         }
1203 
1204     }
1205 
1206     private void copyLocalNativeLibraries( final File localNativeLibrariesDirectory, final File destinationDirectory )
1207             throws MojoExecutionException
1208     {
1209         getLog().debug( "Copying existing native libraries from " + localNativeLibrariesDirectory );
1210         try
1211         {
1212 
1213             IOFileFilter libSuffixFilter = FileFilterUtils.suffixFileFilter( ".so" );
1214 
1215             IOFileFilter gdbserverNameFilter = FileFilterUtils.nameFileFilter( "gdbserver" );
1216             IOFileFilter orFilter = FileFilterUtils.or( libSuffixFilter, gdbserverNameFilter );
1217 
1218             IOFileFilter libFiles = FileFilterUtils.and( FileFileFilter.FILE, orFilter );
1219             FileFilter filter = FileFilterUtils.or( DirectoryFileFilter.DIRECTORY, libFiles );
1220             org.apache.commons.io.FileUtils
1221                     .copyDirectory( localNativeLibrariesDirectory, destinationDirectory, filter );
1222 
1223         }
1224         catch ( IOException e )
1225         {
1226             getLog().error( "Could not copy native libraries: " + e.getMessage(), e );
1227             throw new MojoExecutionException( "Could not copy native dependency.", e );
1228         }
1229     }
1230 
1231 
1232     /**
1233      * Generates an intermediate apk file (actually .ap_) containing the resources and assets.
1234      *
1235      * @throws MojoExecutionException
1236      */
1237     private void generateIntermediateApk() throws MojoExecutionException
1238     {
1239         CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
1240         executor.setLogger( this.getLog() );
1241         File[] overlayDirectories = getResourceOverlayDirectories();
1242 
1243         File androidJar = getAndroidSdk().getAndroidJar();
1244         File outputFile = new File( targetDirectory, finalName + ".ap_" );
1245 
1246         List<File> dependencyArtifactResDirectoryList = new ArrayList<File>();
1247         for ( Artifact libraryArtifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
1248         {
1249             final File libraryResDir = getUnpackedLibResourceFolder( libraryArtifact );
1250             if ( libraryResDir.exists() )
1251             {
1252                 dependencyArtifactResDirectoryList.add( libraryResDir );
1253             }
1254         }
1255 
1256         AaptCommandBuilder commandBuilder = AaptCommandBuilder
1257                 .packageResources( getLog() )
1258                 .forceOverwriteExistingFiles()
1259                 .setPathToAndroidManifest( destinationManifestFile )
1260                 .addResourceDirectoriesIfExists( overlayDirectories )
1261                 .addResourceDirectoryIfExists( resourceDirectory )
1262                 .addResourceDirectoriesIfExists( dependencyArtifactResDirectoryList )
1263                 .autoAddOverlay()
1264                 // NB aapt only accepts a single assets parameter - combinedAssets is a merge of all assets
1265                 .addRawAssetsDirectoryIfExists( combinedAssets )
1266                 .renameManifestPackage( renameManifestPackage )
1267                 .renameInstrumentationTargetPackage( renameInstrumentationTargetPackage )
1268                 .addExistingPackageToBaseIncludeSet( androidJar )
1269                 .setOutputApkFile( outputFile )
1270                 .addConfigurations( configurations )
1271                 .setVerbose( aaptVerbose )
1272                 .setDebugMode( !release )
1273                 .addExtraArguments( aaptExtraArgs );
1274 
1275         getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
1276         try
1277         {
1278             executor.setCaptureStdOut( true );
1279             List<String> commands = commandBuilder.build();
1280             executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
1281         }
1282         catch ( ExecutionException e )
1283         {
1284             throw new MojoExecutionException( "", e );
1285         }
1286     }
1287 
1288     protected AndroidSigner getAndroidSigner()
1289     {
1290         if ( sign == null )
1291         {
1292             return new AndroidSigner( signDebug );
1293         }
1294         else
1295         {
1296             return new AndroidSigner( sign.getDebug() );
1297         }
1298     }
1299 
1300     /**
1301      * Used to populated the {@link #apkMetaInf} attribute via reflection.
1302      */
1303     private MetaInf getDefaultMetaInf()
1304     {
1305         // check for deprecated first
1306         if ( apkMetaIncludes != null && apkMetaIncludes.length > 0 )
1307         {
1308             return new MetaInf().include( apkMetaIncludes );
1309         }
1310 
1311         return this.pluginMetaInf;
1312     }
1313 }