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