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