Home | History | Annotate | Download | only in build
      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 
     62         void reset(File inputFile) {
     63             mInputFile = inputFile;
     64         }
     65 
     66         @Override
     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
     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 
    143         List<String> getNativeLibs() {
    144             return mNativeLibs;
    145         }
    146 
    147         boolean getNativeLibsConflict() {
    148             return mNativeLibsConflict;
    149         }
    150 
    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          */
    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          */
    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 
    193         private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) {
    194             mLibs = libs;
    195             mNativeLibsConflict = nativeLibsConflict;
    196         }
    197 
    198         @Override
    199         public List<String> getNativeLibs() {
    200             return mLibs;
    201         }
    202 
    203         @Override
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    718         FileEntry(File file, String path) {
    719             mFile = file;
    720             mPath = path;
    721         }
    722     }
    723 
    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      */
    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      */
    798     private void verbosePrintln(String format, Object... args) {
    799         if (mVerboseStream != null) {
    800             mVerboseStream.println(String.format(format, args));
    801         }
    802     }
    803 
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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