• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.sdklib.build;
18 
19 import com.android.sdklib.SdkConstants;
20 import com.android.sdklib.internal.build.DebugKeyProvider;
21 import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput;
22 import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
23 import com.android.sdklib.internal.build.SignedJarBuilder;
24 import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter;
25 
26 import java.io.File;
27 import java.io.FileInputStream;
28 import java.io.FileNotFoundException;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.io.PrintStream;
32 import java.security.PrivateKey;
33 import java.security.cert.X509Certificate;
34 import java.text.DateFormat;
35 import java.util.ArrayList;
36 import java.util.Date;
37 import java.util.HashMap;
38 import java.util.List;
39 import java.util.regex.Pattern;
40 
41 /**
42  * Class making the final apk packaging.
43  * The inputs are:
44  * - packaged resources (output of aapt)
45  * - code file (ouput of dx)
46  * - Java resources coming from the project, its libraries, and its jar files
47  * - Native libraries from the project or its library.
48  *
49  */
50 public final class ApkBuilder implements IArchiveBuilder {
51 
52     private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
53             Pattern.CASE_INSENSITIVE);
54 
55     /**
56      * A No-op zip filter. It's used to detect conflicts.
57      *
58      */
59     private final class NullZipFilter implements IZipEntryFilter {
60         private File mInputFile;
61 
reset(File inputFile)62         void reset(File inputFile) {
63             mInputFile = inputFile;
64         }
65 
66         @Override
checkEntry(String archivePath)67         public boolean checkEntry(String archivePath) throws ZipAbortException {
68             verbosePrintln("=> %s", archivePath);
69 
70             File duplicate = checkFileForDuplicate(archivePath);
71             if (duplicate != null) {
72                 throw new DuplicateFileException(archivePath, duplicate, mInputFile);
73             } else {
74                 mAddedFiles.put(archivePath, mInputFile);
75             }
76 
77             return true;
78         }
79     }
80 
81     /**
82      * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java
83      * resources, and also record whether the zip file contains native libraries.
84      * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when
85      * we only want the java resources from external jars.
86      */
87     private final class JavaAndNativeResourceFilter implements IZipEntryFilter {
88         private final List<String> mNativeLibs = new ArrayList<String>();
89         private boolean mNativeLibsConflict = false;
90         private File mInputFile;
91 
92         @Override
checkEntry(String archivePath)93         public boolean checkEntry(String archivePath) throws ZipAbortException {
94             // split the path into segments.
95             String[] segments = archivePath.split("/");
96 
97             // empty path? skip to next entry.
98             if (segments.length == 0) {
99                 return false;
100             }
101 
102             // Check each folders to make sure they should be included.
103             // Folders like CVS, .svn, etc.. should already have been excluded from the
104             // jar file, but we need to exclude some other folder (like /META-INF) so
105             // we check anyway.
106             for (int i = 0 ; i < segments.length - 1; i++) {
107                 if (checkFolderForPackaging(segments[i]) == false) {
108                     return false;
109                 }
110             }
111 
112             // get the file name from the path
113             String fileName = segments[segments.length-1];
114 
115             boolean check = checkFileForPackaging(fileName);
116 
117             // only do additional checks if the file passes the default checks.
118             if (check) {
119                 verbosePrintln("=> %s", archivePath);
120 
121                 File duplicate = checkFileForDuplicate(archivePath);
122                 if (duplicate != null) {
123                     throw new DuplicateFileException(archivePath, duplicate, mInputFile);
124                 } else {
125                     mAddedFiles.put(archivePath, mInputFile);
126                 }
127 
128                 if (archivePath.endsWith(".so")) {
129                     mNativeLibs.add(archivePath);
130 
131                     // only .so located in lib/ will interfere with the installation
132                     if (archivePath.startsWith(SdkConstants.FD_APK_NATIVE_LIBS + "/")) {
133                         mNativeLibsConflict = true;
134                     }
135                 } else if (archivePath.endsWith(".jnilib")) {
136                     mNativeLibs.add(archivePath);
137                 }
138             }
139 
140             return check;
141         }
142 
getNativeLibs()143         List<String> getNativeLibs() {
144             return mNativeLibs;
145         }
146 
getNativeLibsConflict()147         boolean getNativeLibsConflict() {
148             return mNativeLibsConflict;
149         }
150 
reset(File inputFile)151         void reset(File inputFile) {
152             mInputFile = inputFile;
153             mNativeLibs.clear();
154             mNativeLibsConflict = false;
155         }
156     }
157 
158     private File mApkFile;
159     private File mResFile;
160     private File mDexFile;
161     private PrintStream mVerboseStream;
162     private SignedJarBuilder mBuilder;
163     private boolean mDebugMode = false;
164     private boolean mIsSealed = false;
165 
166     private final NullZipFilter mNullFilter = new NullZipFilter();
167     private final JavaAndNativeResourceFilter mFilter = new JavaAndNativeResourceFilter();
168     private final HashMap<String, File> mAddedFiles = new HashMap<String, File>();
169 
170     /**
171      * Status for the addition of a jar file resources into the APK.
172      * This indicates possible issues with native library inside the jar file.
173      */
174     public interface JarStatus {
175         /**
176          * Returns the list of native libraries found in the jar file.
177          */
getNativeLibs()178         List<String> getNativeLibs();
179 
180         /**
181          * Returns whether some of those libraries were located in the location that Android
182          * expects its native libraries.
183          */
hasNativeLibsConflicts()184         boolean hasNativeLibsConflicts();
185 
186     }
187 
188     /** Internal implementation of {@link JarStatus}. */
189     private final static class JarStatusImpl implements JarStatus {
190         public final List<String> mLibs;
191         public final boolean mNativeLibsConflict;
192 
JarStatusImpl(List<String> libs, boolean nativeLibsConflict)193         private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) {
194             mLibs = libs;
195             mNativeLibsConflict = nativeLibsConflict;
196         }
197 
198         @Override
getNativeLibs()199         public List<String> getNativeLibs() {
200             return mLibs;
201         }
202 
203         @Override
hasNativeLibsConflicts()204         public boolean hasNativeLibsConflicts() {
205             return mNativeLibsConflict;
206         }
207     }
208 
209     /**
210      * Signing information.
211      *
212      * Both the {@link PrivateKey} and the {@link X509Certificate} are guaranteed to be non-null.
213      *
214      */
215     public final static class SigningInfo {
216         public final PrivateKey key;
217         public final X509Certificate certificate;
218 
SigningInfo(PrivateKey key, X509Certificate certificate)219         private SigningInfo(PrivateKey key, X509Certificate certificate) {
220             if (key == null || certificate == null) {
221                 throw new IllegalArgumentException("key and certificate cannot be null");
222             }
223             this.key = key;
224             this.certificate = certificate;
225         }
226     }
227 
228     /**
229      * Returns the key and certificate from a given debug store.
230      *
231      * It is expected that the store password is 'android' and the key alias and password are
232      * 'androiddebugkey' and 'android' respectively.
233      *
234      * @param storeOsPath the OS path to the debug store.
235      * @param verboseStream an option {@link PrintStream} to display verbose information
236      * @return they key and certificate in a {@link SigningInfo} object or null.
237      * @throws ApkCreationException
238      */
getDebugKey(String storeOsPath, final PrintStream verboseStream)239     public static SigningInfo getDebugKey(String storeOsPath, final PrintStream verboseStream)
240             throws ApkCreationException {
241         try {
242             if (storeOsPath != null) {
243                 File storeFile = new File(storeOsPath);
244                 try {
245                     checkInputFile(storeFile);
246                 } catch (FileNotFoundException e) {
247                     // ignore these since the debug store can be created on the fly anyway.
248                 }
249 
250                 // get the debug key
251                 if (verboseStream != null) {
252                     verboseStream.println(String.format("Using keystore: %s", storeOsPath));
253                 }
254 
255                 IKeyGenOutput keygenOutput = null;
256                 if (verboseStream != null) {
257                     keygenOutput = new IKeyGenOutput() {
258                         @Override
259                         public void out(String message) {
260                             verboseStream.println(message);
261                         }
262 
263                         @Override
264                         public void err(String message) {
265                             verboseStream.println(message);
266                         }
267                     };
268                 }
269 
270                 DebugKeyProvider keyProvider = new DebugKeyProvider(
271                         storeOsPath, null /*store type*/, keygenOutput);
272 
273                 PrivateKey key = keyProvider.getDebugKey();
274                 X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();
275 
276                 if (key == null) {
277                     throw new ApkCreationException("Unable to get debug signature key");
278                 }
279 
280                 // compare the certificate expiration date
281                 if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
282                     // TODO, regenerate a new one.
283                     throw new ApkCreationException("Debug Certificate expired on " +
284                             DateFormat.getInstance().format(certificate.getNotAfter()));
285                 }
286 
287                 return new SigningInfo(key, certificate);
288             } else {
289                 return null;
290             }
291         } catch (KeytoolException e) {
292             if (e.getJavaHome() == null) {
293                 throw new ApkCreationException(e.getMessage() +
294                         "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" +
295                         "You can also manually execute the following command\n:" +
296                         e.getCommandLine(), e);
297             } else {
298                 throw new ApkCreationException(e.getMessage() +
299                         "\nJAVA_HOME is set to: " + e.getJavaHome() +
300                         "\nUpdate it if necessary, or manually execute the following command:\n" +
301                         e.getCommandLine(), e);
302             }
303         } catch (ApkCreationException e) {
304             throw e;
305         } catch (Exception e) {
306             throw new ApkCreationException(e);
307         }
308     }
309 
310     /**
311      * Creates a new instance.
312      *
313      * This creates a new builder that will create the specified output file, using the two
314      * mandatory given input files.
315      *
316      * An optional debug keystore can be provided. If set, it is expected that the store password
317      * is 'android' and the key alias and password are 'androiddebugkey' and 'android'.
318      *
319      * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
320      * be no output.
321      *
322      * @param apkOsPath the OS path of the file to create.
323      * @param resOsPath the OS path of the packaged resource file.
324      * @param dexOsPath the OS path of the dex file. This can be null for apk with no code.
325      * @param verboseStream the stream to which verbose output should go. If null, verbose mode
326      *                      is not enabled.
327      * @throws ApkCreationException
328      */
ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, String storeOsPath, PrintStream verboseStream)329     public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, String storeOsPath,
330             PrintStream verboseStream) throws ApkCreationException {
331         this(new File(apkOsPath),
332              new File(resOsPath),
333              dexOsPath != null ? new File(dexOsPath) : null,
334              storeOsPath,
335              verboseStream);
336     }
337 
338     /**
339      * Creates a new instance.
340      *
341      * This creates a new builder that will create the specified output file, using the two
342      * mandatory given input files.
343      *
344      * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK.
345      *
346      * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
347      * be no output.
348      *
349      * @param apkOsPath the OS path of the file to create.
350      * @param resOsPath the OS path of the packaged resource file.
351      * @param dexOsPath the OS path of the dex file. This can be null for apk with no code.
352      * @param key the private key used to sign the package. Can be null.
353      * @param certificate the certificate used to sign the package. Can be null.
354      * @param verboseStream the stream to which verbose output should go. If null, verbose mode
355      *                      is not enabled.
356      * @throws ApkCreationException
357      */
ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, PrivateKey key, X509Certificate certificate, PrintStream verboseStream)358     public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, PrivateKey key,
359             X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException {
360         this(new File(apkOsPath),
361              new File(resOsPath),
362              dexOsPath != null ? new File(dexOsPath) : null,
363              key, certificate,
364              verboseStream);
365     }
366 
367     /**
368      * Creates a new instance.
369      *
370      * This creates a new builder that will create the specified output file, using the two
371      * mandatory given input files.
372      *
373      * An optional debug keystore can be provided. If set, it is expected that the store password
374      * is 'android' and the key alias and password are 'androiddebugkey' and 'android'.
375      *
376      * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
377      * be no output.
378      *
379      * @param apkFile the file to create
380      * @param resFile the file representing the packaged resource file.
381      * @param dexFile the file representing the dex file. This can be null for apk with no code.
382      * @param debugStoreOsPath the OS path to the debug keystore, if needed or null.
383      * @param verboseStream the stream to which verbose output should go. If null, verbose mode
384      *                      is not enabled.
385      * @throws ApkCreationException
386      */
ApkBuilder(File apkFile, File resFile, File dexFile, String debugStoreOsPath, final PrintStream verboseStream)387     public ApkBuilder(File apkFile, File resFile, File dexFile, String debugStoreOsPath,
388             final PrintStream verboseStream) throws ApkCreationException {
389 
390         SigningInfo info = getDebugKey(debugStoreOsPath, verboseStream);
391         if (info != null) {
392             init(apkFile, resFile, dexFile, info.key, info.certificate, verboseStream);
393         } else {
394             init(apkFile, resFile, dexFile, null /*key*/, null/*certificate*/, verboseStream);
395         }
396     }
397 
398     /**
399      * Creates a new instance.
400      *
401      * This creates a new builder that will create the specified output file, using the two
402      * mandatory given input files.
403      *
404      * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK.
405      *
406      * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
407      * be no output.
408      *
409      * @param apkFile the file to create
410      * @param resFile the file representing the packaged resource file.
411      * @param dexFile the file representing the dex file. This can be null for apk with no code.
412      * @param key the private key used to sign the package. Can be null.
413      * @param certificate the certificate used to sign the package. Can be null.
414      * @param verboseStream the stream to which verbose output should go. If null, verbose mode
415      *                      is not enabled.
416      * @throws ApkCreationException
417      */
ApkBuilder(File apkFile, File resFile, File dexFile, PrivateKey key, X509Certificate certificate, PrintStream verboseStream)418     public ApkBuilder(File apkFile, File resFile, File dexFile, PrivateKey key,
419             X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException {
420         init(apkFile, resFile, dexFile, key, certificate, verboseStream);
421     }
422 
423 
424     /**
425      * Constructor init method.
426      *
427      * @see #ApkBuilder(File, File, File, String, PrintStream)
428      * @see #ApkBuilder(String, String, String, String, PrintStream)
429      * @see #ApkBuilder(File, File, File, PrivateKey, X509Certificate, PrintStream)
430      */
init(File apkFile, File resFile, File dexFile, PrivateKey key, X509Certificate certificate, PrintStream verboseStream)431     private void init(File apkFile, File resFile, File dexFile, PrivateKey key,
432             X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException {
433 
434         try {
435             checkOutputFile(mApkFile = apkFile);
436             checkInputFile(mResFile = resFile);
437             if (dexFile != null) {
438                 checkInputFile(mDexFile = dexFile);
439             } else {
440                 mDexFile = null;
441             }
442             mVerboseStream = verboseStream;
443 
444             mBuilder = new SignedJarBuilder(
445                     new FileOutputStream(mApkFile, false /* append */), key,
446                     certificate);
447 
448             verbosePrintln("Packaging %s", mApkFile.getName());
449 
450             // add the resources
451             addZipFile(mResFile);
452 
453             // add the class dex file at the root of the apk
454             if (mDexFile != null) {
455                 addFile(mDexFile, SdkConstants.FN_APK_CLASSES_DEX);
456             }
457 
458         } catch (ApkCreationException e) {
459             mBuilder.cleanUp();
460             throw e;
461         } catch (Exception e) {
462             mBuilder.cleanUp();
463             throw new ApkCreationException(e);
464         }
465     }
466 
467     /**
468      * Sets the debug mode. In debug mode, when native libraries are present, the packaging
469      * will also include one or more copies of gdbserver in the final APK file.
470      *
471      * These are used for debugging native code, to ensure that gdbserver is accessible to the
472      * application.
473      *
474      * There will be one version of gdbserver for each ABI supported by the application.
475      *
476      * the gbdserver files are placed in the libs/abi/ folders automatically by the NDK.
477      *
478      * @param debugMode the debug mode flag.
479      */
setDebugMode(boolean debugMode)480     public void setDebugMode(boolean debugMode) {
481         mDebugMode = debugMode;
482     }
483 
484     /**
485      * Adds a file to the APK at a given path
486      * @param file the file to add
487      * @param archivePath the path of the file inside the APK archive.
488      * @throws ApkCreationException if an error occurred
489      * @throws SealedApkException if the APK is already sealed.
490      * @throws DuplicateFileException if a file conflicts with another already added to the APK
491      *                                   at the same location inside the APK archive.
492      */
493     @Override
addFile(File file, String archivePath)494     public void addFile(File file, String archivePath) throws ApkCreationException,
495             SealedApkException, DuplicateFileException {
496         if (mIsSealed) {
497             throw new SealedApkException("APK is already sealed");
498         }
499 
500         try {
501             doAddFile(file, archivePath);
502         } catch (DuplicateFileException e) {
503             mBuilder.cleanUp();
504             throw e;
505         } catch (Exception e) {
506             mBuilder.cleanUp();
507             throw new ApkCreationException(e, "Failed to add %s", file);
508         }
509     }
510 
511     /**
512      * Adds the content from a zip file.
513      * All file keep the same path inside the archive.
514      * @param zipFile the zip File.
515      * @throws ApkCreationException if an error occurred
516      * @throws SealedApkException if the APK is already sealed.
517      * @throws DuplicateFileException if a file conflicts with another already added to the APK
518      *                                   at the same location inside the APK archive.
519      */
addZipFile(File zipFile)520     public void addZipFile(File zipFile) throws ApkCreationException, SealedApkException,
521             DuplicateFileException {
522         if (mIsSealed) {
523             throw new SealedApkException("APK is already sealed");
524         }
525 
526         try {
527             verbosePrintln("%s:", zipFile);
528 
529             // reset the filter with this input.
530             mNullFilter.reset(zipFile);
531 
532             // ask the builder to add the content of the file.
533             FileInputStream fis = new FileInputStream(zipFile);
534             mBuilder.writeZip(fis, mNullFilter);
535         } catch (DuplicateFileException e) {
536             mBuilder.cleanUp();
537             throw e;
538         } catch (Exception e) {
539             mBuilder.cleanUp();
540             throw new ApkCreationException(e, "Failed to add %s", zipFile);
541         }
542     }
543 
544     /**
545      * Adds the resources from a jar file.
546      * @param jarFile the jar File.
547      * @return a {@link JarStatus} object indicating if native libraries where found in
548      *         the jar file.
549      * @throws ApkCreationException if an error occurred
550      * @throws SealedApkException if the APK is already sealed.
551      * @throws DuplicateFileException if a file conflicts with another already added to the APK
552      *                                   at the same location inside the APK archive.
553      */
addResourcesFromJar(File jarFile)554     public JarStatus addResourcesFromJar(File jarFile) throws ApkCreationException,
555             SealedApkException, DuplicateFileException {
556         if (mIsSealed) {
557             throw new SealedApkException("APK is already sealed");
558         }
559 
560         try {
561             verbosePrintln("%s:", jarFile);
562 
563             // reset the filter with this input.
564             mFilter.reset(jarFile);
565 
566             // ask the builder to add the content of the file, filtered to only let through
567             // the java resources.
568             FileInputStream fis = new FileInputStream(jarFile);
569             mBuilder.writeZip(fis, mFilter);
570 
571             // check if native libraries were found in the external library. This should
572             // constitutes an error or warning depending on if they are in lib/
573             return new JarStatusImpl(mFilter.getNativeLibs(), mFilter.getNativeLibsConflict());
574         } catch (DuplicateFileException e) {
575             mBuilder.cleanUp();
576             throw e;
577         } catch (Exception e) {
578             mBuilder.cleanUp();
579             throw new ApkCreationException(e, "Failed to add %s", jarFile);
580         }
581     }
582 
583     /**
584      * Adds the resources from a source folder.
585      * @param sourceFolder the source folder.
586      * @throws ApkCreationException if an error occurred
587      * @throws SealedApkException if the APK is already sealed.
588      * @throws DuplicateFileException if a file conflicts with another already added to the APK
589      *                                   at the same location inside the APK archive.
590      */
addSourceFolder(File sourceFolder)591     public void addSourceFolder(File sourceFolder) throws ApkCreationException, SealedApkException,
592             DuplicateFileException {
593         if (mIsSealed) {
594             throw new SealedApkException("APK is already sealed");
595         }
596 
597         addSourceFolder(this, sourceFolder);
598     }
599 
600     /**
601      * Adds the resources from a source folder to a given {@link IArchiveBuilder}
602      * @param sourceFolder the source folder.
603      * @throws ApkCreationException if an error occurred
604      * @throws SealedApkException if the APK is already sealed.
605      * @throws DuplicateFileException if a file conflicts with another already added to the APK
606      *                                   at the same location inside the APK archive.
607      */
addSourceFolder(IArchiveBuilder builder, File sourceFolder)608     public static void addSourceFolder(IArchiveBuilder builder, File sourceFolder)
609             throws ApkCreationException, DuplicateFileException {
610         if (sourceFolder.isDirectory()) {
611             try {
612                 // file is a directory, process its content.
613                 File[] files = sourceFolder.listFiles();
614                 for (File file : files) {
615                     processFileForResource(builder, file, null);
616                 }
617             } catch (DuplicateFileException e) {
618                 throw e;
619             } catch (Exception e) {
620                 throw new ApkCreationException(e, "Failed to add %s", sourceFolder);
621             }
622         } else {
623             // not a directory? check if it's a file or doesn't exist
624             if (sourceFolder.exists()) {
625                 throw new ApkCreationException("%s is not a folder", sourceFolder);
626             } else {
627                 throw new ApkCreationException("%s does not exist", sourceFolder);
628             }
629         }
630     }
631 
632     /**
633      * Adds the native libraries from the top native folder.
634      * The content of this folder must be the various ABI folders.
635      *
636      * This may or may not copy gdbserver into the apk based on whether the debug mode is set.
637      *
638      * @param nativeFolder the native folder.
639      *
640      * @throws ApkCreationException if an error occurred
641      * @throws SealedApkException if the APK is already sealed.
642      * @throws DuplicateFileException if a file conflicts with another already added to the APK
643      *                                   at the same location inside the APK archive.
644      *
645      * @see #setDebugMode(boolean)
646      */
addNativeLibraries(File nativeFolder)647     public void addNativeLibraries(File nativeFolder)
648             throws ApkCreationException, SealedApkException, DuplicateFileException {
649         if (mIsSealed) {
650             throw new SealedApkException("APK is already sealed");
651         }
652 
653         if (nativeFolder.isDirectory() == false) {
654             // not a directory? check if it's a file or doesn't exist
655             if (nativeFolder.exists()) {
656                 throw new ApkCreationException("%s is not a folder", nativeFolder);
657             } else {
658                 throw new ApkCreationException("%s does not exist", nativeFolder);
659             }
660         }
661 
662         File[] abiList = nativeFolder.listFiles();
663 
664         verbosePrintln("Native folder: %s", nativeFolder);
665 
666         if (abiList != null) {
667             for (File abi : abiList) {
668                 if (abi.isDirectory()) { // ignore files
669 
670                     File[] libs = abi.listFiles();
671                     if (libs != null) {
672                         for (File lib : libs) {
673                             // only consider files that are .so or, if in debug mode, that
674                             // are gdbserver executables
675                             if (lib.isFile() &&
676                                     (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() ||
677                                             (mDebugMode &&
678                                                     SdkConstants.FN_GDBSERVER.equals(
679                                                             lib.getName())))) {
680                                 String path =
681                                     SdkConstants.FD_APK_NATIVE_LIBS + "/" +
682                                     abi.getName() + "/" + lib.getName();
683 
684                                 try {
685                                     doAddFile(lib, path);
686                                 } catch (IOException e) {
687                                     mBuilder.cleanUp();
688                                     throw new ApkCreationException(e, "Failed to add %s", lib);
689                                 }
690                             }
691                         }
692                     }
693                 }
694             }
695         }
696     }
697 
addNativeLibraries(List<FileEntry> entries)698     public void addNativeLibraries(List<FileEntry> entries) throws SealedApkException,
699             DuplicateFileException, ApkCreationException {
700         if (mIsSealed) {
701             throw new SealedApkException("APK is already sealed");
702         }
703 
704         for (FileEntry entry : entries) {
705             try {
706                 doAddFile(entry.mFile, entry.mPath);
707             } catch (IOException e) {
708                 mBuilder.cleanUp();
709                 throw new ApkCreationException(e, "Failed to add %s", entry.mFile);
710             }
711         }
712     }
713 
714     public static final class FileEntry {
715         public final File mFile;
716         public final String mPath;
717 
FileEntry(File file, String path)718         FileEntry(File file, String path) {
719             mFile = file;
720             mPath = path;
721         }
722     }
723 
getNativeFiles(File nativeFolder, boolean debugMode)724     public static List<FileEntry> getNativeFiles(File nativeFolder, boolean debugMode)
725             throws ApkCreationException  {
726 
727         if (nativeFolder.isDirectory() == false) {
728             // not a directory? check if it's a file or doesn't exist
729             if (nativeFolder.exists()) {
730                 throw new ApkCreationException("%s is not a folder", nativeFolder);
731             } else {
732                 throw new ApkCreationException("%s does not exist", nativeFolder);
733             }
734         }
735 
736         List<FileEntry> files = new ArrayList<FileEntry>();
737 
738         File[] abiList = nativeFolder.listFiles();
739 
740         if (abiList != null) {
741             for (File abi : abiList) {
742                 if (abi.isDirectory()) { // ignore files
743 
744                     File[] libs = abi.listFiles();
745                     if (libs != null) {
746                         for (File lib : libs) {
747                             // only consider files that are .so or, if in debug mode, that
748                             // are gdbserver executables
749                             if (lib.isFile() &&
750                                     (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() ||
751                                             (debugMode &&
752                                                     SdkConstants.FN_GDBSERVER.equals(
753                                                             lib.getName())))) {
754                                 String path =
755                                     SdkConstants.FD_APK_NATIVE_LIBS + "/" +
756                                     abi.getName() + "/" + lib.getName();
757 
758                                 files.add(new FileEntry(lib, path));
759                             }
760                         }
761                     }
762                 }
763             }
764         }
765 
766         return files;
767     }
768 
769 
770 
771     /**
772      * Seals the APK, and signs it if necessary.
773      * @throws ApkCreationException
774      * @throws ApkCreationException if an error occurred
775      * @throws SealedApkException if the APK is already sealed.
776      */
sealApk()777     public void sealApk() throws ApkCreationException, SealedApkException {
778         if (mIsSealed) {
779             throw new SealedApkException("APK is already sealed");
780         }
781 
782         // close and sign the application package.
783         try {
784             mBuilder.close();
785             mIsSealed = true;
786         } catch (Exception e) {
787             throw new ApkCreationException(e, "Failed to seal APK");
788         } finally {
789             mBuilder.cleanUp();
790         }
791     }
792 
793     /**
794      * Output a given message if the verbose mode is enabled.
795      * @param format the format string for {@link String#format(String, Object...)}
796      * @param args the string arguments
797      */
verbosePrintln(String format, Object... args)798     private void verbosePrintln(String format, Object... args) {
799         if (mVerboseStream != null) {
800             mVerboseStream.println(String.format(format, args));
801         }
802     }
803 
doAddFile(File file, String archivePath)804     private void doAddFile(File file, String archivePath) throws DuplicateFileException,
805             IOException {
806         verbosePrintln("%1$s => %2$s", file, archivePath);
807 
808         File duplicate = checkFileForDuplicate(archivePath);
809         if (duplicate != null) {
810             throw new DuplicateFileException(archivePath, duplicate, file);
811         }
812 
813         mAddedFiles.put(archivePath, file);
814         mBuilder.writeFile(file, archivePath);
815     }
816 
817     /**
818      * Processes a {@link File} that could be an APK {@link File}, or a folder containing
819      * java resources.
820      *
821      * @param file the {@link File} to process.
822      * @param path the relative path of this file to the source folder.
823      *          Can be <code>null</code> to identify a root file.
824      * @throws IOException
825      * @throws DuplicateFileException if a file conflicts with another already added
826      *          to the APK at the same location inside the APK archive.
827      * @throws SealedApkException if the APK is already sealed.
828      * @throws ApkCreationException if an error occurred
829      */
processFileForResource(IArchiveBuilder builder, File file, String path)830     private static void processFileForResource(IArchiveBuilder builder, File file, String path)
831             throws IOException, DuplicateFileException, ApkCreationException, SealedApkException {
832         if (file.isDirectory()) {
833             // a directory? we check it
834             if (checkFolderForPackaging(file.getName())) {
835                 // if it's valid, we append its name to the current path.
836                 if (path == null) {
837                     path = file.getName();
838                 } else {
839                     path = path + "/" + file.getName();
840                 }
841 
842                 // and process its content.
843                 File[] files = file.listFiles();
844                 for (File contentFile : files) {
845                     processFileForResource(builder, contentFile, path);
846                 }
847             }
848         } else {
849             // a file? we check it to make sure it should be added
850             if (checkFileForPackaging(file.getName())) {
851                 // we append its name to the current path
852                 if (path == null) {
853                     path = file.getName();
854                 } else {
855                     path = path + "/" + file.getName();
856                 }
857 
858                 // and add it to the apk
859                 builder.addFile(file, path);
860             }
861         }
862     }
863 
864     /**
865      * Checks if the given path in the APK archive has not already been used and if it has been,
866      * then returns a {@link File} object for the source of the duplicate
867      * @param archivePath the archive path to test.
868      * @return A File object of either a file at the same location or an archive that contains a
869      * file that was put at the same location.
870      */
checkFileForDuplicate(String archivePath)871     private File checkFileForDuplicate(String archivePath) {
872         return mAddedFiles.get(archivePath);
873     }
874 
875     /**
876      * Checks an output {@link File} object.
877      * This checks the following:
878      * - the file is not an existing directory.
879      * - if the file exists, that it can be modified.
880      * - if it doesn't exists, that a new file can be created.
881      * @param file the File to check
882      * @throws ApkCreationException If the check fails
883      */
checkOutputFile(File file)884     private void checkOutputFile(File file) throws ApkCreationException {
885         if (file.isDirectory()) {
886             throw new ApkCreationException("%s is a directory!", file);
887         }
888 
889         if (file.exists()) { // will be a file in this case.
890             if (file.canWrite() == false) {
891                 throw new ApkCreationException("Cannot write %s", file);
892             }
893         } else {
894             try {
895                 if (file.createNewFile() == false) {
896                     throw new ApkCreationException("Failed to create %s", file);
897                 }
898             } catch (IOException e) {
899                 throw new ApkCreationException(
900                         "Failed to create '%1$ss': %2$s", file, e.getMessage());
901             }
902         }
903     }
904 
905     /**
906      * Checks an input {@link File} object.
907      * This checks the following:
908      * - the file is not an existing directory.
909      * - that the file exists (if <var>throwIfDoesntExist</var> is <code>false</code>) and can
910      *    be read.
911      * @param file the File to check
912      * @throws FileNotFoundException if the file is not here.
913      * @throws ApkCreationException If the file is a folder or a file that cannot be read.
914      */
checkInputFile(File file)915     private static void checkInputFile(File file) throws FileNotFoundException, ApkCreationException {
916         if (file.isDirectory()) {
917             throw new ApkCreationException("%s is a directory!", file);
918         }
919 
920         if (file.exists()) {
921             if (file.canRead() == false) {
922                 throw new ApkCreationException("Cannot read %s", file);
923             }
924         } else {
925             throw new FileNotFoundException(String.format("%s does not exist", file));
926         }
927     }
928 
getDebugKeystore()929     public static String getDebugKeystore() throws ApkCreationException {
930         try {
931             return DebugKeyProvider.getDefaultKeyStoreOsPath();
932         } catch (Exception e) {
933             throw new ApkCreationException(e, e.getMessage());
934         }
935     }
936 
937     /**
938      * Checks whether a folder and its content is valid for packaging into the .apk as
939      * standard Java resource.
940      * @param folderName the name of the folder.
941      */
checkFolderForPackaging(String folderName)942     public static boolean checkFolderForPackaging(String folderName) {
943         return folderName.equalsIgnoreCase("CVS") == false &&
944             folderName.equalsIgnoreCase(".svn") == false &&
945             folderName.equalsIgnoreCase("SCCS") == false &&
946             folderName.equalsIgnoreCase("META-INF") == false &&
947             folderName.startsWith("_") == false;
948     }
949 
950     /**
951      * Checks a file to make sure it should be packaged as standard resources.
952      * @param fileName the name of the file (including extension)
953      * @return true if the file should be packaged as standard java resources.
954      */
checkFileForPackaging(String fileName)955     public static boolean checkFileForPackaging(String fileName) {
956         String[] fileSegments = fileName.split("\\.");
957         String fileExt = "";
958         if (fileSegments.length > 1) {
959             fileExt = fileSegments[fileSegments.length-1];
960         }
961 
962         return checkFileForPackaging(fileName, fileExt);
963     }
964 
965     /**
966      * Checks a file to make sure it should be packaged as standard resources.
967      * @param fileName the name of the file (including extension)
968      * @param extension the extension of the file (excluding '.')
969      * @return true if the file should be packaged as standard java resources.
970      */
checkFileForPackaging(String fileName, String extension)971     public static boolean checkFileForPackaging(String fileName, String extension) {
972         // ignore hidden files and backup files
973         if (fileName.charAt(0) == '.' || fileName.charAt(fileName.length()-1) == '~') {
974             return false;
975         }
976 
977         return "aidl".equalsIgnoreCase(extension) == false &&       // Aidl files
978             "rs".equalsIgnoreCase(extension) == false &&            // RenderScript files
979             "rsh".equalsIgnoreCase(extension) == false &&           // RenderScript header files
980             "d".equalsIgnoreCase(extension) == false &&             // Dependency files
981             "java".equalsIgnoreCase(extension) == false &&          // Java files
982             "scala".equalsIgnoreCase(extension) == false &&         // Scala files
983             "class".equalsIgnoreCase(extension) == false &&         // Java class files
984             "scc".equalsIgnoreCase(extension) == false &&           // VisualSourceSafe
985             "swp".equalsIgnoreCase(extension) == false &&           // vi swap file
986             "thumbs.db".equalsIgnoreCase(fileName) == false &&      // image index file
987             "picasa.ini".equalsIgnoreCase(fileName) == false &&     // image index file
988             "package.html".equalsIgnoreCase(fileName) == false &&   // Javadoc
989             "overview.html".equalsIgnoreCase(fileName) == false;    // Javadoc
990     }
991 }
992