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.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 
     62         void reset(File inputFile) {
     63             mInputFile = inputFile;
     64         }
     65 
     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 
     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 
    141         List<String> getNativeLibs() {
    142             return mNativeLibs;
    143         }
    144 
    145         boolean getNativeLibsConflict() {
    146             return mNativeLibsConflict;
    147         }
    148 
    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          */
    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          */
    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 
    191         private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) {
    192             mLibs = libs;
    193             mNativeLibsConflict = nativeLibsConflict;
    194         }
    195 
    196         public List<String> getNativeLibs() {
    197             return mLibs;
    198         }
    199 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    688         FileEntry(File file, String path) {
    689             mFile = file;
    690             mPath = path;
    691         }
    692     }
    693 
    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      */
    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      */
    766     private void verbosePrintln(String format, Object... args) {
    767         if (mVerboseStream != null) {
    768             mVerboseStream.println(String.format(format, args));
    769         }
    770     }
    771 
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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