1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
88
89
90
91
92
93 @Mojo( name = "apk",
94 defaultPhase = LifecyclePhase.PACKAGE,
95 requiresDependencyResolution = ResolutionScope.COMPILE )
96 public class ApkMojo extends AbstractAndroidMojo
97 {
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118 @Parameter
119 private Sign sign;
120
121
122
123
124
125
126 @Parameter( property = "android.sign.debug", defaultValue = "auto", readonly = true )
127 private String signDebug;
128
129
130
131
132
133
134
135
136 @Parameter( property = "android.renameInstrumentationTargetPackage" )
137 private String renameInstrumentationTargetPackage;
138
139
140
141
142
143
144
145 @Parameter( property = "android.extractDuplicates", defaultValue = "false" )
146 private boolean extractDuplicates;
147
148
149
150
151 @Parameter
152 private String classifier;
153
154
155
156
157
158 @Parameter( property = "android.outputApk",
159 defaultValue = "${project.build.directory}/${project.build.finalName}.apk" )
160 private String outputApk;
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178 @Parameter( property = "android.sourceDirectories" )
179 private File[] sourceDirectories;
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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
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
233
234 @Parameter( property = "android.ndk.build.build.final-library.name" )
235 private String ndkFinalLibraryName;
236
237
238
239
240
241
242
243
244 @Parameter
245 private String[] excludeJarResources;
246
247 private Pattern[] excludeJarResourcesPatterns;
248
249
250
251
252 @Parameter
253 @ConfigPojo( prefix = "apk" )
254 private Apk apk;
255
256
257
258
259
260 @Parameter( property = "skipDependencies", defaultValue = "false" )
261 private boolean skipDependencies;
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278 @Parameter( property = "artifactTypeSet" )
279 private IncludeExcludeSet artifactTypeSet;
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
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
312
313
314
315 @Parameter( property = "android.nativeLibrariesDependenciesHardwareArchitectureDefault", defaultValue = "armeabi" )
316 private String nativeLibrariesDependenciesHardwareArchitectureDefault;
317
318 @Parameter
319 private ResourceTransformer[] transformers;
320
321
322
323
324
325 public void execute() throws MojoExecutionException, MojoFailureException
326 {
327
328
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
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
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
376 project.getArtifact().setFile( outputFile );
377 }
378 else
379 {
380
381 projectHelper.attachArtifact( project, AndroidExtension.APK, classifier, outputFile );
382 }
383 }
384
385 void createApkFile( File outputFile, boolean signWithDebugKeyStore ) throws MojoExecutionException
386 {
387
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
403
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
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 {
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;
580 }
581 lPath = lPath.substring( rPath.length() + 1 );
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
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
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
663
664
665
666
667
668
669
670
671
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
680
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
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
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
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
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
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
936 if ( ! duplicates.contains( entry.getName() ) )
937 {
938
939 jos.putNextEntry( entry );
940 InputStream currIn = inZip.getInputStream( entry );
941 copyStreamWithoutClosing( currIn, jos );
942 currIn.close();
943 jos.closeEntry();
944 }
945
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
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
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 );
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
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
1063
1064
1065
1066
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
1086
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
1096 final File folder = getUnpackedLibNativesFolder( resolvedArtifact );
1097 getLog().debug( "Adding native library folder " + folder );
1098 natives.add( folder );
1099 }
1100
1101
1102 for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES )
1103 {
1104 if ( NativeHelper.artifactHasHardwareArchitecture( resolvedArtifact,
1105 ndkArchitecture, nativeLibrariesDependenciesHardwareArchitectureDefault ) )
1106 {
1107
1108 copyNativeLibraryArtifact( resolvedArtifact, ndkOutputDirectory, ndkArchitecture );
1109 }
1110 }
1111 }
1112
1113 if ( apkDebug )
1114 {
1115
1116 for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES )
1117 {
1118 copyGdbServer( ndkOutputDirectory, ndkArchitecture );
1119 }
1120 }
1121
1122 if ( ndkOutputDirectory.exists() )
1123 {
1124
1125 getLog().debug( "Adding built native library folder " + ndkOutputDirectory );
1126 natives.add( ndkOutputDirectory );
1127 }
1128
1129 return natives;
1130 }
1131
1132
1133
1134
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
1157
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
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
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
1234
1235
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
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
1302
1303 private MetaInf getDefaultMetaInf()
1304 {
1305
1306 if ( apkMetaIncludes != null && apkMetaIncludes.length > 0 )
1307 {
1308 return new MetaInf().include( apkMetaIncludes );
1309 }
1310
1311 return this.pluginMetaInf;
1312 }
1313 }