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.phase08preparepackage;
18  
19  import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts;
20  import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
21  import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
22  import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Set;
30  
31  import org.apache.commons.io.FileUtils;
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.maven.artifact.Artifact;
34  import org.apache.maven.model.Resource;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugins.annotations.LifecyclePhase;
38  import org.apache.maven.plugins.annotations.Mojo;
39  import org.apache.maven.plugins.annotations.Parameter;
40  import org.apache.maven.plugins.annotations.ResolutionScope;
41  import org.codehaus.plexus.archiver.ArchiverException;
42  import org.codehaus.plexus.archiver.jar.JarArchiver;
43  import org.codehaus.plexus.archiver.util.DefaultFileSet;
44  
45  import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
46  import com.simpligility.maven.plugins.android.CommandExecutor;
47  import com.simpligility.maven.plugins.android.ExecutionException;
48  import com.simpligility.maven.plugins.android.IncludeExcludeSet;
49  import com.simpligility.maven.plugins.android.common.Const;
50  import com.simpligility.maven.plugins.android.common.ZipExtractor;
51  import com.simpligility.maven.plugins.android.configuration.Dex;
52  
53  /**
54   * Converts compiled Java classes to the Android dex format.
55   *
56   * @author hugo.josefson@jayway.com
57   */
58  @Mojo(
59      name = "dex",
60      defaultPhase = LifecyclePhase.PREPARE_PACKAGE,
61      requiresDependencyResolution = ResolutionScope.COMPILE
62  )
63  public class DexMojo extends AbstractAndroidMojo
64  {
65  
66      /**
67       * Configuration for the dex command execution. It can be configured in the plugin configuration like so
68       *
69       * <pre>
70       * &lt;dexCompiler&gt;dex&lt;/dexCompiler&gt;
71       * &lt;dex&gt;
72       *   &lt;jvmArguments&gt;
73       *     &lt;jvmArgument&gt;-Xms256m&lt;/jvmArgument&gt;
74       *     &lt;jvmArgument&gt;-Xmx512m&lt;/jvmArgument&gt;
75       *   &lt;/jvmArguments&gt;
76       *   &lt;coreLibrary&gt;true|false&lt;/coreLibrary&gt;
77       *   &lt;noLocals&gt;true|false&lt;/noLocals&gt;
78       *   &lt;forceJumbo&gt;true|false&lt;/forceJumbo&gt;
79       *   &lt;optimize&gt;true|false&lt;/optimize&gt;
80       *   &lt;preDex&gt;true|false&lt;/preDex&gt;
81       *   &lt;preDexLibLocation&gt;path to predexed libraries, defaults to target/dexedLibs&lt;/preDexLibLocation&gt;
82       *   &lt;incremental&gt;true|false&lt;/incremental&gt;
83       *   &lt;multiDex&gt;true|false&lt;/multiDex&gt;
84       *   &lt;generateMainDexList&gt;true|false&lt;/generateMainDexList&gt;
85       *   &lt;mainDexList&gt;path to class list file&lt;/mainDexList&gt;
86       *   &lt;minimalMainDex&gt;true|false&lt;/minimalMainDex&gt;
87       * &lt;/dex&gt;
88       * </pre>
89       *
90       * or via properties dex.* or command line parameters android.dex.*
91       */
92  
93      /**
94       * The dex compiler to use. Allowed values are 'dex' (default) and 'd8'.
95       */
96      @Parameter( property = "android.dex.compiler", defaultValue = "dex" )
97      private String dexCompiler;
98  
99      @Parameter
100     private Dex dex;
101 
102     /**
103      * Extra JVM Arguments. Using these you can e.g. increase memory for the jvm running the build.
104      */
105     @Parameter( property = "android.dex.jvmArguments", defaultValue = "-Xmx1024M" )
106     private String[] dexJvmArguments;
107 
108     /**
109      * Decides whether to pass the --core-library flag to dx.
110      */
111     @Parameter( property = "android.dex.coreLibrary", defaultValue = "false" )
112     private boolean dexCoreLibrary;
113 
114     /**
115      * Decides whether to pass the --no-locals flag to dx.
116      */
117     @Parameter( property = "android.dex.noLocals", defaultValue = "false" )
118     private boolean dexNoLocals;
119 
120     /**
121      * Decides whether to pass the --no-optimize flag to dx.
122      */
123     @Parameter( property = "android.dex.optimize", defaultValue = "true" )
124     private boolean dexOptimize;
125 
126     /**
127      * Decides whether to predex the jars.
128      */
129     @Parameter( property = "android.dex.predex", defaultValue = "false" )
130     private boolean dexPreDex;
131 
132     /**
133      * Decides whether to use force jumbo mode.
134      */
135     @Parameter( property = "android.dex.forcejumbo", defaultValue = "false" )
136     private boolean dexForceJumbo;
137 
138     /**
139      * Path to predexed libraries.
140      */
141     @Parameter(
142         property = "android.dex.dexPreDexLibLocation",
143         defaultValue = "${project.build.directory}${file.separator}dexedLibs"
144     )
145     private String dexPreDexLibLocation;
146 
147     /**
148      * Decides whether to pass the --incremental flag to dx.
149      */
150     @Parameter( property = "android.dex.incremental", defaultValue = "false" )
151     private boolean dexIncremental;
152 
153     /**
154      * The name of the obfuscated JAR.
155      */
156     @Parameter( property = "android.proguard.obfuscatedJar" )
157     private File obfuscatedJar;
158 
159     /**
160      * Decides whether to pass the --multi-dex flag to dx.
161      */
162     @Parameter( property = "android.dex.multidex", defaultValue = "false" )
163     private boolean dexMultiDex;
164 
165     /**
166      * Full path to class list to multi dex
167      */
168     @Parameter( property = "android.dex.maindexlist" )
169     private String dexMainDexList;
170 
171     /**
172      * Decides whether to pass the --minimal-main-dex flag to dx.
173      */
174     @Parameter( property = "android.dex.minimalmaindex", defaultValue = "false" )
175     private boolean dexMinimalMainDex;
176 
177     /**
178      * Decides whether to generate main dex list.
179      * Supported from build tools version 22.0.0+
180      *
181      * Note: if set to true, dexMinimalMainDex is set to true, and dexMainDexList
182      * is set to generated main dex list.
183      */
184     @Parameter( property = "android.dex.generatemaindexlist", defaultValue = "false" )
185     private boolean dexGenerateMainDexList;
186 
187     /**
188      * Additional command line parameters passed to dx.
189      */
190     @Parameter( property = "android.dex.dexarguments" )
191     private String dexArguments;
192 
193     /**
194      * Skips transitive dependencies. May be useful if the target classes directory is populated with the
195      * {@code maven-dependency-plugin} and already contains all dependency classes.
196      */
197     @Parameter( property = "skipDependencies", defaultValue = "false" )
198     private boolean skipDependencies;
199 
200     /**
201      * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the
202      * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty
203      * strings are ignored. Example:
204      * <pre>
205      *     &lt;artifactTypeSet&gt;
206      *         &lt;includes&gt;
207      *             &lt;include&gt;aar&lt;/include&gt;
208      *         &lt;includes&gt;
209      *         &lt;excludes&gt;
210      *             &lt;exclude&gt;jar&lt;/exclude&gt;
211      *         &lt;excludes&gt;
212      *     &lt;/artifactTypeSet&gt;
213      * </pre>
214      */
215     @Parameter( property = "artifactTypeSet" )
216     private IncludeExcludeSet artifactTypeSet;
217 
218     /**
219      * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The
220      * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can
221      * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId},
222      * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using
223      * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact
224      * range. Empty strings are ignored. Example:
225      * <pre>
226      *     &lt;artifactTypeSet&gt;
227      *         &lt;includes&gt;
228      *             &lt;include&gt;foo-group:foo-artifact:1.0-SNAPSHOT&lt;/include&gt;
229      *             &lt;include&gt;bar-group:bar-artifact:1.0-SNAPSHOT&lt;/include&gt;
230      *             &lt;include&gt;baz-group:*&lt;/include&gt;
231      *         &lt;includes&gt;
232      *         &lt;excludes&gt;
233      *             &lt;exclude&gt;qux-group:qux-artifact:*&lt;/exclude&gt;
234      *         &lt;excludes&gt;
235      *     &lt;/artifactTypeSet&gt;
236      * </pre>
237      */
238     @Parameter( property = "artifactSet" )
239     private IncludeExcludeSet artifactSet;
240 
241     private String[] parsedJvmArguments;
242     private boolean parsedCoreLibrary;
243     private boolean parsedNoLocals;
244     private boolean parsedOptimize;
245     private boolean parsedPreDex;
246     private boolean parsedForceJumbo;
247     private String parsedPreDexLibLocation;
248     private boolean parsedIncremental;
249     private boolean parsedMultiDex;
250     private String parsedMainDexList;
251     private boolean parsedMinimalMainDex;
252     private boolean parsedGenerateMainDexList;
253     private String parsedDexArguments;
254     private DexCompiler parsedDexCompiler;
255 
256     /**
257      * @throws MojoExecutionException
258      * @throws MojoFailureException
259      */
260     @Override
261     public void execute() throws MojoExecutionException, MojoFailureException
262     {
263         parseConfiguration();
264 
265         getLog().debug( "DexCompiler set to " + parsedDexCompiler );
266         if ( parsedDexCompiler != DexCompiler.DEX )
267         {
268             getLog().info( "Not executing DexMojo because DEX compiler is set to " + parsedDexCompiler );
269             return;
270         }
271 
272         if ( getJack().isEnabled() )
273         {
274             //Dexxing is handled by Jack
275             return;
276         }
277 
278         CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
279         executor.setLogger( getLog() );
280 
281         File outputFile;
282         if ( parsedMultiDex )
283         {
284             outputFile = targetDirectory;
285             if ( parsedGenerateMainDexList )
286             {
287                 getAndroidSdk().assertThatBuildToolsVersionIsAtLeast(
288                     "22.0.0", "generate main dex list" );
289                 File generatedMainDexClassesList = generateMainDexClassesList( executor );
290                 parsedMainDexList = generatedMainDexClassesList.getAbsolutePath();
291                 parsedMinimalMainDex = true;
292             }
293         }
294         else
295         {
296             outputFile = new File( targetDirectory, "classes.dex" );
297         }
298         if ( generateApk )
299         {
300             runDex( executor, outputFile );
301         }
302 
303         if ( attachJar )
304         {
305             File jarFile = new File( targetDirectory + File.separator
306                 + finalName + ".jar" );
307             projectHelper.attachArtifact( project, "jar", project.getArtifact().getClassifier(), jarFile );
308         }
309 
310         if ( attachSources )
311         {
312             // Also attach an .apksources, containing sources from this project.
313             final File apksources = createApkSourcesFile();
314             projectHelper.attachArtifact( project, "apksources", apksources );
315         }
316     }
317 
318     /**
319      * Gets the input files for dex. This is a combination of directories and jar files.
320      *
321      * @return
322      */
323     private Set< File > getDexInputFiles() throws MojoExecutionException
324     {
325         Set< File > inputs = new HashSet< File >();
326 
327         if ( obfuscatedJar != null && obfuscatedJar.exists() )
328         {
329             // proguard has been run, use this jar
330             getLog().debug( "Adding dex input (obfuscatedJar) : " + obfuscatedJar );
331             inputs.add( obfuscatedJar );
332         }
333         else
334         {
335             getLog().debug( "Using non-obfuscated input" );
336             // no proguard, use original config
337             inputs.add( projectOutputDirectory );
338             getLog().debug( "Adding dex input : " + project.getBuild().getOutputDirectory() );
339             for ( Artifact artifact : filterArtifacts( getTransitiveDependencyArtifacts(), skipDependencies,
340                 artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
341                 artifactSet.getExcludes() ) )
342             {
343                 if ( artifact.getType().equals( Const.ArtifactType.NATIVE_SYMBOL_OBJECT )
344                     || artifact.getType().equals( Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE ) )
345                 {
346                     // Ignore native dependencies - no need for dexer to see those
347                 }
348                 else if ( artifact.getType().equals( APKLIB ) )
349                 {
350                     // Any jars in the libs folder should now be
351                     // automatically included because they will be a transitive dependency.
352                 }
353                 else if ( artifact.getType().equals( AAR ) )
354                 {
355                     // The Aar classes.jar should now be automatically included
356                     // because it will be a transitive dependency. As should any jars in the libs folder.
357                 }
358                 else if ( artifact.getType().equals( APK ) )
359                 {
360                     // We need to dex the APK classes including the APK R.
361                     // But we don't want to add a second instance of the embedded Rs for any of the APK's dependencies
362                     // as they will already have been generated to target/classes. The R values from the APK will be
363                     // the correct ones, so best solution is to extract the APK classes (including all Rs) to
364                     // target/classes overwriting any generated Rs and let dex pick up the values from there.
365                     getLog().debug( "Extracting APK classes to target/classes : " + artifact.getArtifactId() );
366                     final File apkClassesJar = getUnpackedLibHelper().getJarFileForApk( artifact );
367                     getLog().debug( "Extracting APK : " + apkClassesJar + " to " + targetDirectory );
368                     final ZipExtractor extractor = new ZipExtractor( getLog() );
369                     extractor.extract( apkClassesJar, targetDirectory, ".class" );
370                 }
371                 else
372                 {
373                     getLog().debug( "Adding dex input : " + artifact.getFile() );
374                     inputs.add( artifact.getFile().getAbsoluteFile() );
375                 }
376             }
377         }
378 
379         return inputs;
380     }
381 
382     private void parseConfiguration()
383     {
384         // config in pom found
385         if ( dex != null )
386         {
387             // the if statements make sure that properties/command line
388             // parameter overrides configuration
389             // and that the dafaults apply in all cases;
390             if ( dex.getJvmArguments() == null )
391             {
392                 parsedJvmArguments = dexJvmArguments;
393             }
394             else
395             {
396                 parsedJvmArguments = dex.getJvmArguments();
397             }
398             if ( dex.isCoreLibrary() == null )
399             {
400                 parsedCoreLibrary = dexCoreLibrary;
401             }
402             else
403             {
404                 parsedCoreLibrary = dex.isCoreLibrary();
405             }
406             if ( dex.isNoLocals() == null )
407             {
408                 parsedNoLocals = dexNoLocals;
409             }
410             else
411             {
412                 parsedNoLocals = dex.isNoLocals();
413             }
414             if ( dex.isOptimize() == null )
415             {
416                 parsedOptimize = dexOptimize;
417             }
418             else
419             {
420                 parsedOptimize = dex.isOptimize();
421             }
422             if ( dex.isPreDex() == null )
423             {
424                 parsedPreDex = dexPreDex;
425             }
426             else
427             {
428                 parsedPreDex = dex.isPreDex();
429             }
430             if ( dex.getPreDexLibLocation() == null )
431             {
432                 parsedPreDexLibLocation = dexPreDexLibLocation;
433             }
434             else
435             {
436                 parsedPreDexLibLocation = dex.getPreDexLibLocation();
437             }
438             if ( dex.isIncremental() == null )
439             {
440                 parsedIncremental = dexIncremental;
441             }
442             else
443             {
444                 parsedIncremental = dex.isIncremental();
445             }
446             if ( dex.isForceJumbo() == null )
447             {
448                 parsedForceJumbo = dexForceJumbo;
449             }
450             else
451             {
452                 parsedForceJumbo = dex.isForceJumbo();
453             }
454             if ( dex.isMultiDex() == null )
455             {
456                 parsedMultiDex = dexMultiDex;
457             }
458             else
459             {
460                 parsedMultiDex = dex.isMultiDex();
461             }
462             if ( dex.getMainDexList() == null )
463             {
464                 parsedMainDexList = dexMainDexList;
465             }
466             else
467             {
468                 parsedMainDexList = dex.getMainDexList();
469             }
470             if ( dex.isMinimalMainDex() == null )
471             {
472                 parsedMinimalMainDex = dexMinimalMainDex;
473             }
474             else
475             {
476                 parsedMinimalMainDex = dex.isMinimalMainDex();
477             }
478             if ( dex.isGenerateMainDexList() == null )
479             {
480                 parsedGenerateMainDexList = dexGenerateMainDexList;
481             }
482             else
483             {
484                 parsedGenerateMainDexList = dex.isGenerateMainDexList();
485             }
486             if ( dex.getDexArguments() == null )
487             {
488                 parsedDexArguments = dexArguments;
489             }
490             else
491             {
492                 parsedDexArguments = dex.getDexArguments();
493             }
494             parsedDexCompiler = DexCompiler.valueOfIgnoreCase( dexCompiler );
495 
496         }
497         else
498         {
499             parsedJvmArguments = dexJvmArguments;
500             parsedCoreLibrary = dexCoreLibrary;
501             parsedNoLocals = dexNoLocals;
502             parsedOptimize = dexOptimize;
503             parsedPreDex = dexPreDex;
504             parsedPreDexLibLocation = dexPreDexLibLocation;
505             parsedIncremental = dexIncremental;
506             parsedForceJumbo = dexForceJumbo;
507             parsedMultiDex = dexMultiDex;
508             parsedMainDexList = dexMainDexList;
509             parsedMinimalMainDex = dexMinimalMainDex;
510             parsedGenerateMainDexList = dexGenerateMainDexList;
511             parsedDexArguments = dexArguments;
512             parsedDexCompiler = DexCompiler.valueOfIgnoreCase( dexCompiler );
513         }
514     }
515 
516     private Set< File > preDex( CommandExecutor executor, Set< File > inputFiles ) throws MojoExecutionException
517     {
518         Set< File > filtered = new HashSet< File >();
519         getLog().info( "Pre dex-ing libraries for faster dex-ing of the final application." );
520 
521         for ( File inputFile : inputFiles )
522         {
523             if ( inputFile.getName().matches( ".*\\.jar$" ) )
524             {
525                 List< String > commands = dexDefaultCommands();
526 
527                 File predexJar = predexJarPath( inputFile );
528                 commands.add( "--output=" + predexJar.getAbsolutePath() );
529                 commands.add( inputFile.getAbsolutePath() );
530                 filtered.add( predexJar );
531 
532                 if ( !predexJar.isFile() || predexJar.lastModified() < inputFile.lastModified() )
533                 {
534                     getLog().info( "Pre-dex ing jar: " + inputFile.getAbsolutePath() );
535                     executeJava( commands, executor );
536                 }
537             }
538             else
539             {
540                 filtered.add( inputFile );
541             }
542         }
543 
544         return filtered;
545     }
546 
547     private File predexJarPath( File inputFile )
548     {
549         final File predexLibsDirectory = new File( parsedPreDexLibLocation.trim() );
550         predexLibsDirectory.mkdirs();
551         return new File( predexLibsDirectory, inputFile.getName() );
552     }
553 
554     private List< String > dexDefaultCommands() throws MojoExecutionException
555     {
556         List< String > commands = jarDefaultCommands();
557         commands.add( getAndroidSdk().getDxJarPath() );
558         commands.add( "--dex" );
559         return commands;
560     }
561 
562     private List<String> jarDefaultCommands()
563     {
564         List< String > commands = javaDefaultCommands();
565         commands.add( "-jar" );
566         return commands;
567     }
568 
569     private List<String> javaDefaultCommands()
570     {
571         List< String > commands = new ArrayList< String > ();
572         if ( parsedJvmArguments != null )
573         {
574             for ( String jvmArgument : parsedJvmArguments )
575             {
576                 // preserve backward compatibility allowing argument with or
577                 // without dash (e.g. Xmx512m as well as
578                 // -Xmx512m should work) (see
579                 // http://code.google.com/p/maven-android-plugin/issues/detail?id=153)
580                 if ( !jvmArgument.startsWith( "-" ) )
581                 {
582                     jvmArgument = "-" + jvmArgument;
583                 }
584                 getLog().debug( "Adding jvm argument " + jvmArgument );
585                 commands.add( jvmArgument );
586             }
587         }
588         return commands;
589     }
590 
591     private void runDex( CommandExecutor executor, File outputFile )
592         throws MojoExecutionException
593     {
594         final List< String > commands = dexDefaultCommands();
595         final Set< File > inputFiles = getDexInputFiles();
596         Set< File > filteredFiles = inputFiles;
597         if ( parsedPreDex )
598         {
599             filteredFiles = preDex( executor, inputFiles );
600         }
601         if ( !parsedOptimize )
602         {
603             commands.add( "--no-optimize" );
604         }
605         if ( parsedCoreLibrary )
606         {
607             commands.add( "--core-library" );
608         }
609         if ( parsedIncremental )
610         {
611             commands.add( "--incremental" );
612         }
613         if ( parsedNoLocals )
614         {
615             commands.add( "--no-locals" );
616         }
617         if ( parsedForceJumbo )
618         {
619             commands.add( "--force-jumbo" );
620         }
621         if ( parsedMultiDex )
622         {
623             commands.add( "--multi-dex" );
624             if ( parsedMainDexList != null )
625             {
626                 commands.add( "--main-dex-list=" + parsedMainDexList );
627             }
628             if ( parsedMinimalMainDex )
629             {
630                 commands.add( "--minimal-main-dex" );
631             }
632         }
633         if ( parsedDexArguments != null )
634         {
635             commands.add( parsedDexArguments );
636         }
637         commands.add( "--output=" + outputFile.getAbsolutePath() );
638         for ( File inputFile : filteredFiles )
639         {
640             commands.add( inputFile.getAbsolutePath() );
641         }
642 
643         getLog().info( "Convert classes to Dex : " + outputFile );
644         executeJava( commands, executor );
645     }
646 
647     private String executeJava( final List<String> commands, CommandExecutor executor ) throws MojoExecutionException
648     {
649         final String javaExecutable = getJavaExecutable().getAbsolutePath();
650         getLog().debug( javaExecutable + " " + commands.toString() );
651         try
652         {
653             executor.setCaptureStdOut( true );
654             executor.executeCommand( javaExecutable, commands, project.getBasedir(), false );
655             return executor.getStandardOut();
656         }
657         catch ( ExecutionException e )
658         {
659             throw new MojoExecutionException( "", e );
660         }
661     }
662 
663     /**
664      * Figure out the full path to the current java executable.
665      *
666      * @return the full path to the current java executable.
667      */
668     private static File getJavaExecutable()
669     {
670         final String javaHome = System.getProperty( "java.home" );
671         final String slash = File.separator;
672         return new File( javaHome + slash + "bin" + slash + "java" );
673     }
674 
675     private File generateMainDexClassesJar( CommandExecutor executor ) throws MojoExecutionException
676     {
677         List< String> commands = jarDefaultCommands();
678         commands.add( getAndroidSdk().getProguardJarPath() );
679         commands.add( "-dontnote" );
680         commands.add( "-dontwarn" );
681         commands.add( "-forceprocessing" );
682         commands.add( "-dontoptimize" );
683         commands.add( "-dontpreverify" );
684         commands.add( "-dontobfuscate" );
685 
686         Set< File> inputFiles = getDexInputFiles();
687         for ( File inputFile : inputFiles )
688         {
689             commands.add( "-injars" );
690             commands.add( inputFile.getAbsolutePath() + "(!META-INF/**)" );
691         }
692 
693         commands.add( "-libraryjars" );
694         commands.add( getAndroidSdk().getShrinkedAndroidJarPath() );
695 
696         commands.add( "-include" );
697         commands.add( getAndroidSdk().getMainDexClassesRulesPath() );
698 
699         commands.add( "-outjars" );
700         File mainDexClassesJar = new File( targetDirectory, "mainDexClasses.jar" );
701         commands.add( mainDexClassesJar.getAbsolutePath() );
702 
703         getLog().info( "Generating main dex classes jar : " + mainDexClassesJar );
704         executeJava( commands, executor );
705 
706         return mainDexClassesJar;
707     }
708 
709     private File generateMainDexClassesList( CommandExecutor executor ) throws MojoExecutionException
710     {
711         File mainDexClassesJar = generateMainDexClassesJar( executor );
712         List< String> commands = javaDefaultCommands();
713 
714         commands.add( "-Djava.ext.dirs=" + getAndroidSdk().getBuildToolsLibDirectoryPath() );
715         commands.add( "com.android.multidex.MainDexListBuilder" );
716         commands.add( mainDexClassesJar.getAbsolutePath() );
717 
718         Set< File> inputFiles = getDexInputFiles();
719         StringBuilder sb = new StringBuilder();
720         sb.append( StringUtils.join( inputFiles, File.pathSeparatorChar ) );
721         commands.add( sb.toString() );
722 
723         File mainDexClassesList = new File( targetDirectory, "mainDexClasses.txt" );
724 
725         getLog().info( "Generating main dex classes list : " + mainDexClassesList );
726 
727         String output = executeJava( commands, executor );
728         try
729         {
730             FileUtils.writeStringToFile( mainDexClassesList, output );
731         }
732         catch ( IOException ex )
733         {
734             throw new MojoExecutionException( "Failed to write command output with main dex classes list to "
735                 + mainDexClassesList, ex );
736         }
737         return mainDexClassesList;
738     }
739 
740     /**
741      * @return
742      * @throws MojoExecutionException
743      */
744     protected File createApkSourcesFile() throws MojoExecutionException
745     {
746         final File apksources = new File( targetDirectory, finalName
747             + ".apksources" );
748         FileUtils.deleteQuietly( apksources );
749 
750         try
751         {
752             JarArchiver jarArchiver = new JarArchiver();
753             jarArchiver.setDestFile( apksources );
754 
755             addDirectory( jarArchiver, assetsDirectory, "assets" );
756             addDirectory( jarArchiver, resourceDirectory, "res" );
757             addDirectory( jarArchiver, sourceDirectory, "src/main/java" );
758             addJavaResources( jarArchiver, resources );
759 
760             jarArchiver.createArchive();
761         }
762         catch ( ArchiverException e )
763         {
764             throw new MojoExecutionException( "ArchiverException while creating .apksource file.", e );
765         }
766         catch ( IOException e )
767         {
768             throw new MojoExecutionException( "IOException while creating .apksource file.", e );
769         }
770 
771         return apksources;
772     }
773 
774     /**
775      * Makes sure the string ends with "/"
776      *
777      * @param prefix
778      *            any string, or null.
779      * @return the prefix with a "/" at the end, never null.
780      */
781     protected String endWithSlash( String prefix )
782     {
783         prefix = StringUtils.defaultIfEmpty( prefix, "/" );
784         if ( !prefix.endsWith( "/" ) )
785         {
786             prefix = prefix + "/";
787         }
788         return prefix;
789     }
790 
791     /**
792      * Adds a directory to a {@link JarArchiver} with a directory prefix.
793      *
794      * @param jarArchiver
795      * @param directory
796      *            The directory to add.
797      * @param prefix
798      *            An optional prefix for where in the Jar file the directory's contents should go.
799      */
800     protected void addDirectory( JarArchiver jarArchiver, File directory, String prefix )
801     {
802         if ( directory != null && directory.exists() )
803         {
804             final DefaultFileSet fileSet = new DefaultFileSet();
805             fileSet.setPrefix( endWithSlash( prefix ) );
806             fileSet.setDirectory( directory );
807             jarArchiver.addFileSet( fileSet );
808         }
809     }
810 
811     /**
812      * @param jarArchiver
813      * @param javaResources
814      */
815     protected void addJavaResources( JarArchiver jarArchiver, List< Resource > javaResources )
816     {
817         for ( Resource javaResource : javaResources )
818         {
819             addJavaResource( jarArchiver, javaResource );
820         }
821     }
822 
823     /**
824      * Adds a Java Resources directory (typically "src/main/resources") to a {@link JarArchiver}.
825      *
826      * @param jarArchiver
827      * @param javaResource
828      *            The Java resource to add.
829      */
830     protected void addJavaResource( JarArchiver jarArchiver, Resource javaResource )
831     {
832         if ( javaResource != null )
833         {
834             final File javaResourceDirectory = new File( javaResource.getDirectory() );
835             if ( javaResourceDirectory.exists() )
836             {
837                 final DefaultFileSet javaResourceFileSet = new DefaultFileSet();
838                 javaResourceFileSet.setDirectory( javaResourceDirectory );
839                 javaResourceFileSet.setPrefix( endWithSlash( "src/main/resources" ) );
840                 jarArchiver.addFileSet( javaResourceFileSet );
841             }
842         }
843     }
844 }