| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.sdklib.build; |
| |
| import com.android.sdklib.SdkConstants; |
| import com.android.sdklib.internal.build.DebugKeyProvider; |
| import com.android.sdklib.internal.build.SignedJarBuilder; |
| import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; |
| import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; |
| import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.security.PrivateKey; |
| import java.security.cert.X509Certificate; |
| import java.text.DateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Class making the final apk packaging. |
| * The inputs are: |
| * - packaged resources (output of aapt) |
| * - code file (ouput of dx) |
| * - Java resources coming from the project, its libraries, and its jar files |
| * - Native libraries from the project or its library. |
| * |
| */ |
| public final class ApkBuilder implements IArchiveBuilder { |
| |
| private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$", |
| Pattern.CASE_INSENSITIVE); |
| |
| /** |
| * A No-op zip filter. It's used to detect conflicts. |
| * |
| */ |
| private final class NullZipFilter implements IZipEntryFilter { |
| private File mInputFile; |
| |
| void reset(File inputFile) { |
| mInputFile = inputFile; |
| } |
| |
| public boolean checkEntry(String archivePath) throws ZipAbortException { |
| verbosePrintln("=> %s", archivePath); |
| |
| File duplicate = checkFileForDuplicate(archivePath); |
| if (duplicate != null) { |
| throw new DuplicateFileException(archivePath, duplicate, mInputFile); |
| } else { |
| mAddedFiles.put(archivePath, mInputFile); |
| } |
| |
| return true; |
| } |
| } |
| |
| /** |
| * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java |
| * resources, and also record whether the zip file contains native libraries. |
| * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when |
| * we only want the java resources from external jars. |
| */ |
| private final class JavaAndNativeResourceFilter implements IZipEntryFilter { |
| private final List<String> mNativeLibs = new ArrayList<String>(); |
| private boolean mNativeLibsConflict = false; |
| private File mInputFile; |
| |
| public boolean checkEntry(String archivePath) throws ZipAbortException { |
| // split the path into segments. |
| String[] segments = archivePath.split("/"); |
| |
| // empty path? skip to next entry. |
| if (segments.length == 0) { |
| return false; |
| } |
| |
| // Check each folders to make sure they should be included. |
| // Folders like CVS, .svn, etc.. should already have been excluded from the |
| // jar file, but we need to exclude some other folder (like /META-INF) so |
| // we check anyway. |
| for (int i = 0 ; i < segments.length - 1; i++) { |
| if (checkFolderForPackaging(segments[i]) == false) { |
| return false; |
| } |
| } |
| |
| // get the file name from the path |
| String fileName = segments[segments.length-1]; |
| |
| boolean check = checkFileForPackaging(fileName); |
| |
| // only do additional checks if the file passes the default checks. |
| if (check) { |
| verbosePrintln("=> %s", archivePath); |
| |
| File duplicate = checkFileForDuplicate(archivePath); |
| if (duplicate != null) { |
| throw new DuplicateFileException(archivePath, duplicate, mInputFile); |
| } else { |
| mAddedFiles.put(archivePath, mInputFile); |
| } |
| |
| if (archivePath.endsWith(".so")) { |
| mNativeLibs.add(archivePath); |
| |
| // only .so located in lib/ will interfere with the installation |
| if (archivePath.startsWith(SdkConstants.FD_APK_NATIVE_LIBS + "/")) { |
| mNativeLibsConflict = true; |
| } |
| } else if (archivePath.endsWith(".jnilib")) { |
| mNativeLibs.add(archivePath); |
| } |
| } |
| |
| return check; |
| } |
| |
| List<String> getNativeLibs() { |
| return mNativeLibs; |
| } |
| |
| boolean getNativeLibsConflict() { |
| return mNativeLibsConflict; |
| } |
| |
| void reset(File inputFile) { |
| mInputFile = inputFile; |
| mNativeLibs.clear(); |
| mNativeLibsConflict = false; |
| } |
| } |
| |
| private File mApkFile; |
| private File mResFile; |
| private File mDexFile; |
| private PrintStream mVerboseStream; |
| private SignedJarBuilder mBuilder; |
| private boolean mDebugMode = false; |
| private boolean mIsSealed = false; |
| |
| private final NullZipFilter mNullFilter = new NullZipFilter(); |
| private final JavaAndNativeResourceFilter mFilter = new JavaAndNativeResourceFilter(); |
| private final HashMap<String, File> mAddedFiles = new HashMap<String, File>(); |
| |
| /** |
| * Status for the addition of a jar file resources into the APK. |
| * This indicates possible issues with native library inside the jar file. |
| */ |
| public interface JarStatus { |
| /** |
| * Returns the list of native libraries found in the jar file. |
| */ |
| List<String> getNativeLibs(); |
| |
| /** |
| * Returns whether some of those libraries were located in the location that Android |
| * expects its native libraries. |
| */ |
| boolean hasNativeLibsConflicts(); |
| |
| } |
| |
| /** Internal implementation of {@link JarStatus}. */ |
| private final static class JarStatusImpl implements JarStatus { |
| public final List<String> mLibs; |
| public final boolean mNativeLibsConflict; |
| |
| private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) { |
| mLibs = libs; |
| mNativeLibsConflict = nativeLibsConflict; |
| } |
| |
| public List<String> getNativeLibs() { |
| return mLibs; |
| } |
| |
| public boolean hasNativeLibsConflicts() { |
| return mNativeLibsConflict; |
| } |
| } |
| |
| /** |
| * Signing information. |
| * |
| * Both the {@link PrivateKey} and the {@link X509Certificate} are guaranteed to be non-null. |
| * |
| */ |
| public final static class SigningInfo { |
| public final PrivateKey key; |
| public final X509Certificate certificate; |
| |
| private SigningInfo(PrivateKey key, X509Certificate certificate) { |
| if (key == null || certificate == null) { |
| throw new IllegalArgumentException("key and certificate cannot be null"); |
| } |
| this.key = key; |
| this.certificate = certificate; |
| } |
| } |
| |
| /** |
| * Returns the key and certificate from a given debug store. |
| * |
| * It is expected that the store password is 'android' and the key alias and password are |
| * 'androiddebugkey' and 'android' respectively. |
| * |
| * @param storeOsPath the OS path to the debug store. |
| * @param verboseStream an option {@link PrintStream} to display verbose information |
| * @return they key and certificate in a {@link SigningInfo} object or null. |
| * @throws ApkCreationException |
| */ |
| public static SigningInfo getDebugKey(String storeOsPath, final PrintStream verboseStream) |
| throws ApkCreationException { |
| try { |
| if (storeOsPath != null) { |
| File storeFile = new File(storeOsPath); |
| try { |
| checkInputFile(storeFile); |
| } catch (FileNotFoundException e) { |
| // ignore these since the debug store can be created on the fly anyway. |
| } |
| |
| // get the debug key |
| if (verboseStream != null) { |
| verboseStream.println(String.format("Using keystore: %s", storeOsPath)); |
| } |
| |
| IKeyGenOutput keygenOutput = null; |
| if (verboseStream != null) { |
| keygenOutput = new IKeyGenOutput() { |
| public void out(String message) { |
| verboseStream.println(message); |
| } |
| |
| public void err(String message) { |
| verboseStream.println(message); |
| } |
| }; |
| } |
| |
| DebugKeyProvider keyProvider = new DebugKeyProvider( |
| storeOsPath, null /*store type*/, keygenOutput); |
| |
| PrivateKey key = keyProvider.getDebugKey(); |
| X509Certificate certificate = (X509Certificate)keyProvider.getCertificate(); |
| |
| if (key == null) { |
| throw new ApkCreationException("Unable to get debug signature key"); |
| } |
| |
| // compare the certificate expiration date |
| if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) { |
| // TODO, regenerate a new one. |
| throw new ApkCreationException("Debug Certificate expired on " + |
| DateFormat.getInstance().format(certificate.getNotAfter())); |
| } |
| |
| return new SigningInfo(key, certificate); |
| } else { |
| return null; |
| } |
| } catch (KeytoolException e) { |
| if (e.getJavaHome() == null) { |
| throw new ApkCreationException(e.getMessage() + |
| "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" + |
| "You can also manually execute the following command\n:" + |
| e.getCommandLine(), e); |
| } else { |
| throw new ApkCreationException(e.getMessage() + |
| "\nJAVA_HOME is set to: " + e.getJavaHome() + |
| "\nUpdate it if necessary, or manually execute the following command:\n" + |
| e.getCommandLine(), e); |
| } |
| } catch (ApkCreationException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new ApkCreationException(e); |
| } |
| } |
| |
| /** |
| * Creates a new instance. |
| * |
| * This creates a new builder that will create the specified output file, using the two |
| * mandatory given input files. |
| * |
| * An optional debug keystore can be provided. If set, it is expected that the store password |
| * is 'android' and the key alias and password are 'androiddebugkey' and 'android'. |
| * |
| * An optional {@link PrintStream} can also be provided for verbose output. If null, there will |
| * be no output. |
| * |
| * @param apkOsPath the OS path of the file to create. |
| * @param resOsPath the OS path of the packaged resource file. |
| * @param dexOsPath the OS path of the dex file. This can be null for apk with no code. |
| * @param verboseStream the stream to which verbose output should go. If null, verbose mode |
| * is not enabled. |
| * @throws ApkCreationException |
| */ |
| public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, String storeOsPath, |
| PrintStream verboseStream) throws ApkCreationException { |
| this(new File(apkOsPath), |
| new File(resOsPath), |
| dexOsPath != null ? new File(dexOsPath) : null, |
| storeOsPath, |
| verboseStream); |
| } |
| |
| /** |
| * Creates a new instance. |
| * |
| * This creates a new builder that will create the specified output file, using the two |
| * mandatory given input files. |
| * |
| * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK. |
| * |
| * An optional {@link PrintStream} can also be provided for verbose output. If null, there will |
| * be no output. |
| * |
| * @param apkOsPath the OS path of the file to create. |
| * @param resOsPath the OS path of the packaged resource file. |
| * @param dexOsPath the OS path of the dex file. This can be null for apk with no code. |
| * @param key the private key used to sign the package. Can be null. |
| * @param certificate the certificate used to sign the package. Can be null. |
| * @param verboseStream the stream to which verbose output should go. If null, verbose mode |
| * is not enabled. |
| * @throws ApkCreationException |
| */ |
| public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, PrivateKey key, |
| X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException { |
| this(new File(apkOsPath), |
| new File(resOsPath), |
| dexOsPath != null ? new File(dexOsPath) : null, |
| key, certificate, |
| verboseStream); |
| } |
| |
| /** |
| * Creates a new instance. |
| * |
| * This creates a new builder that will create the specified output file, using the two |
| * mandatory given input files. |
| * |
| * An optional debug keystore can be provided. If set, it is expected that the store password |
| * is 'android' and the key alias and password are 'androiddebugkey' and 'android'. |
| * |
| * An optional {@link PrintStream} can also be provided for verbose output. If null, there will |
| * be no output. |
| * |
| * @param apkFile the file to create |
| * @param resFile the file representing the packaged resource file. |
| * @param dexFile the file representing the dex file. This can be null for apk with no code. |
| * @param debugStoreOsPath the OS path to the debug keystore, if needed or null. |
| * @param verboseStream the stream to which verbose output should go. If null, verbose mode |
| * is not enabled. |
| * @throws ApkCreationException |
| */ |
| public ApkBuilder(File apkFile, File resFile, File dexFile, String debugStoreOsPath, |
| final PrintStream verboseStream) throws ApkCreationException { |
| |
| SigningInfo info = getDebugKey(debugStoreOsPath, verboseStream); |
| if (info != null) { |
| init(apkFile, resFile, dexFile, info.key, info.certificate, verboseStream); |
| } else { |
| init(apkFile, resFile, dexFile, null /*key*/, null/*certificate*/, verboseStream); |
| } |
| } |
| |
| /** |
| * Creates a new instance. |
| * |
| * This creates a new builder that will create the specified output file, using the two |
| * mandatory given input files. |
| * |
| * Optional {@link PrivateKey} and {@link X509Certificate} can be provided to sign the APK. |
| * |
| * An optional {@link PrintStream} can also be provided for verbose output. If null, there will |
| * be no output. |
| * |
| * @param apkFile the file to create |
| * @param resFile the file representing the packaged resource file. |
| * @param dexFile the file representing the dex file. This can be null for apk with no code. |
| * @param key the private key used to sign the package. Can be null. |
| * @param certificate the certificate used to sign the package. Can be null. |
| * @param verboseStream the stream to which verbose output should go. If null, verbose mode |
| * is not enabled. |
| * @throws ApkCreationException |
| */ |
| public ApkBuilder(File apkFile, File resFile, File dexFile, PrivateKey key, |
| X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException { |
| init(apkFile, resFile, dexFile, key, certificate, verboseStream); |
| } |
| |
| |
| /** |
| * Constructor init method. |
| * |
| * @see #ApkBuilder(File, File, File, String, PrintStream) |
| * @see #ApkBuilder(String, String, String, String, PrintStream) |
| * @see #ApkBuilder(File, File, File, PrivateKey, X509Certificate, PrintStream) |
| */ |
| private void init(File apkFile, File resFile, File dexFile, PrivateKey key, |
| X509Certificate certificate, PrintStream verboseStream) throws ApkCreationException { |
| |
| try { |
| checkOutputFile(mApkFile = apkFile); |
| checkInputFile(mResFile = resFile); |
| if (dexFile != null) { |
| checkInputFile(mDexFile = dexFile); |
| } else { |
| mDexFile = null; |
| } |
| mVerboseStream = verboseStream; |
| |
| mBuilder = new SignedJarBuilder( |
| new FileOutputStream(mApkFile, false /* append */), key, |
| certificate); |
| |
| verbosePrintln("Packaging %s", mApkFile.getName()); |
| |
| // add the resources |
| addZipFile(mResFile); |
| |
| // add the class dex file at the root of the apk |
| if (mDexFile != null) { |
| addFile(mDexFile, SdkConstants.FN_APK_CLASSES_DEX); |
| } |
| |
| } catch (ApkCreationException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new ApkCreationException(e); |
| } |
| } |
| |
| /** |
| * Sets the debug mode. In debug mode, when native libraries are present, the packaging |
| * will also include one or more copies of gdbserver in the final APK file. |
| * |
| * These are used for debugging native code, to ensure that gdbserver is accessible to the |
| * application. |
| * |
| * There will be one version of gdbserver for each ABI supported by the application. |
| * |
| * the gbdserver files are placed in the libs/abi/ folders automatically by the NDK. |
| * |
| * @param debugMode the debug mode flag. |
| */ |
| public void setDebugMode(boolean debugMode) { |
| mDebugMode = debugMode; |
| } |
| |
| /** |
| * Adds a file to the APK at a given path |
| * @param file the file to add |
| * @param archivePath the path of the file inside the APK archive. |
| * @throws ApkCreationException if an error occurred |
| * @throws SealedApkException if the APK is already sealed. |
| * @throws DuplicateFileException if a file conflicts with another already added to the APK |
| * at the same location inside the APK archive. |
| */ |
| public void addFile(File file, String archivePath) throws ApkCreationException, |
| SealedApkException, DuplicateFileException { |
| if (mIsSealed) { |
| throw new SealedApkException("APK is already sealed"); |
| } |
| |
| try { |
| doAddFile(file, archivePath); |
| } catch (DuplicateFileException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new ApkCreationException(e, "Failed to add %s", file); |
| } |
| } |
| |
| /** |
| * Adds the content from a zip file. |
| * All file keep the same path inside the archive. |
| * @param zipFile the zip File. |
| * @throws ApkCreationException if an error occurred |
| * @throws SealedApkException if the APK is already sealed. |
| * @throws DuplicateFileException if a file conflicts with another already added to the APK |
| * at the same location inside the APK archive. |
| */ |
| public void addZipFile(File zipFile) throws ApkCreationException, SealedApkException, |
| DuplicateFileException { |
| if (mIsSealed) { |
| throw new SealedApkException("APK is already sealed"); |
| } |
| |
| try { |
| verbosePrintln("%s:", zipFile); |
| |
| // reset the filter with this input. |
| mNullFilter.reset(zipFile); |
| |
| // ask the builder to add the content of the file. |
| FileInputStream fis = new FileInputStream(zipFile); |
| mBuilder.writeZip(fis, mNullFilter); |
| } catch (DuplicateFileException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new ApkCreationException(e, "Failed to add %s", zipFile); |
| } |
| } |
| |
| /** |
| * Adds the resources from a jar file. |
| * @param jarFile the jar File. |
| * @return a {@link JarStatus} object indicating if native libraries where found in |
| * the jar file. |
| * @throws ApkCreationException if an error occurred |
| * @throws SealedApkException if the APK is already sealed. |
| * @throws DuplicateFileException if a file conflicts with another already added to the APK |
| * at the same location inside the APK archive. |
| */ |
| public JarStatus addResourcesFromJar(File jarFile) throws ApkCreationException, |
| SealedApkException, DuplicateFileException { |
| if (mIsSealed) { |
| throw new SealedApkException("APK is already sealed"); |
| } |
| |
| try { |
| verbosePrintln("%s:", jarFile); |
| |
| // reset the filter with this input. |
| mFilter.reset(jarFile); |
| |
| // ask the builder to add the content of the file, filtered to only let through |
| // the java resources. |
| FileInputStream fis = new FileInputStream(jarFile); |
| mBuilder.writeZip(fis, mFilter); |
| |
| // check if native libraries were found in the external library. This should |
| // constitutes an error or warning depending on if they are in lib/ |
| return new JarStatusImpl(mFilter.getNativeLibs(), mFilter.getNativeLibsConflict()); |
| } catch (DuplicateFileException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new ApkCreationException(e, "Failed to add %s", jarFile); |
| } |
| } |
| |
| /** |
| * Adds the resources from a source folder. |
| * @param sourceFolder the source folder. |
| * @throws ApkCreationException if an error occurred |
| * @throws SealedApkException if the APK is already sealed. |
| * @throws DuplicateFileException if a file conflicts with another already added to the APK |
| * at the same location inside the APK archive. |
| */ |
| public void addSourceFolder(File sourceFolder) throws ApkCreationException, SealedApkException, |
| DuplicateFileException { |
| if (mIsSealed) { |
| throw new SealedApkException("APK is already sealed"); |
| } |
| |
| if (sourceFolder.isDirectory()) { |
| try { |
| // file is a directory, process its content. |
| File[] files = sourceFolder.listFiles(); |
| for (File file : files) { |
| processFileForResource(file, null); |
| } |
| } catch (DuplicateFileException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new ApkCreationException(e, "Failed to add %s", sourceFolder); |
| } |
| } else { |
| // not a directory? check if it's a file or doesn't exist |
| if (sourceFolder.exists()) { |
| throw new ApkCreationException("%s is not a folder", sourceFolder); |
| } else { |
| throw new ApkCreationException("%s does not exist", sourceFolder); |
| } |
| } |
| } |
| |
| /** |
| * Adds the native libraries from the top native folder. |
| * The content of this folder must be the various ABI folders. |
| * |
| * This may or may not copy gdbserver into the apk based on whether the debug mode is set. |
| * |
| * @param nativeFolder the native folder. |
| * |
| * @throws ApkCreationException if an error occurred |
| * @throws SealedApkException if the APK is already sealed. |
| * @throws DuplicateFileException if a file conflicts with another already added to the APK |
| * at the same location inside the APK archive. |
| * |
| * @see #setDebugMode(boolean) |
| */ |
| public void addNativeLibraries(File nativeFolder) |
| throws ApkCreationException, SealedApkException, DuplicateFileException { |
| if (mIsSealed) { |
| throw new SealedApkException("APK is already sealed"); |
| } |
| |
| if (nativeFolder.isDirectory() == false) { |
| // not a directory? check if it's a file or doesn't exist |
| if (nativeFolder.exists()) { |
| throw new ApkCreationException("%s is not a folder", nativeFolder); |
| } else { |
| throw new ApkCreationException("%s does not exist", nativeFolder); |
| } |
| } |
| |
| File[] abiList = nativeFolder.listFiles(); |
| |
| verbosePrintln("Native folder: %s", nativeFolder); |
| |
| if (abiList != null) { |
| for (File abi : abiList) { |
| if (abi.isDirectory()) { // ignore files |
| |
| File[] libs = abi.listFiles(); |
| if (libs != null) { |
| for (File lib : libs) { |
| // only consider files that are .so or, if in debug mode, that |
| // are gdbserver executables |
| if (lib.isFile() && |
| (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() || |
| (mDebugMode && |
| SdkConstants.FN_GDBSERVER.equals( |
| lib.getName())))) { |
| String path = |
| SdkConstants.FD_APK_NATIVE_LIBS + "/" + |
| abi.getName() + "/" + lib.getName(); |
| |
| try { |
| doAddFile(lib, path); |
| } catch (IOException e) { |
| throw new ApkCreationException(e, "Failed to add %s", lib); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| public void addNativeLibraries(List<FileEntry> entries) throws SealedApkException, |
| DuplicateFileException, ApkCreationException { |
| if (mIsSealed) { |
| throw new SealedApkException("APK is already sealed"); |
| } |
| |
| for (FileEntry entry : entries) { |
| try { |
| doAddFile(entry.mFile, entry.mPath); |
| } catch (IOException e) { |
| throw new ApkCreationException(e, "Failed to add %s", entry.mFile); |
| } |
| } |
| } |
| |
| public static final class FileEntry { |
| public final File mFile; |
| public final String mPath; |
| |
| FileEntry(File file, String path) { |
| mFile = file; |
| mPath = path; |
| } |
| } |
| |
| public static List<FileEntry> getNativeFiles(File nativeFolder, boolean debugMode) |
| throws ApkCreationException { |
| |
| if (nativeFolder.isDirectory() == false) { |
| // not a directory? check if it's a file or doesn't exist |
| if (nativeFolder.exists()) { |
| throw new ApkCreationException("%s is not a folder", nativeFolder); |
| } else { |
| throw new ApkCreationException("%s does not exist", nativeFolder); |
| } |
| } |
| |
| List<FileEntry> files = new ArrayList<FileEntry>(); |
| |
| File[] abiList = nativeFolder.listFiles(); |
| |
| if (abiList != null) { |
| for (File abi : abiList) { |
| if (abi.isDirectory()) { // ignore files |
| |
| File[] libs = abi.listFiles(); |
| if (libs != null) { |
| for (File lib : libs) { |
| // only consider files that are .so or, if in debug mode, that |
| // are gdbserver executables |
| if (lib.isFile() && |
| (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() || |
| (debugMode && |
| SdkConstants.FN_GDBSERVER.equals( |
| lib.getName())))) { |
| String path = |
| SdkConstants.FD_APK_NATIVE_LIBS + "/" + |
| abi.getName() + "/" + lib.getName(); |
| |
| files.add(new FileEntry(lib, path)); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| return files; |
| } |
| |
| |
| |
| /** |
| * Seals the APK, and signs it if necessary. |
| * @throws ApkCreationException |
| * @throws ApkCreationException if an error occurred |
| * @throws SealedApkException if the APK is already sealed. |
| */ |
| public void sealApk() throws ApkCreationException, SealedApkException { |
| if (mIsSealed) { |
| throw new SealedApkException("APK is already sealed"); |
| } |
| |
| // close and sign the application package. |
| try { |
| mBuilder.close(); |
| mIsSealed = true; |
| } catch (Exception e) { |
| throw new ApkCreationException(e, "Failed to seal APK"); |
| } |
| } |
| |
| /** |
| * Output a given message if the verbose mode is enabled. |
| * @param format the format string for {@link String#format(String, Object...)} |
| * @param args the string arguments |
| */ |
| private void verbosePrintln(String format, Object... args) { |
| if (mVerboseStream != null) { |
| mVerboseStream.println(String.format(format, args)); |
| } |
| } |
| |
| private void doAddFile(File file, String archivePath) throws DuplicateFileException, |
| IOException { |
| verbosePrintln("%1$s => %2$s", file, archivePath); |
| |
| File duplicate = checkFileForDuplicate(archivePath); |
| if (duplicate != null) { |
| throw new DuplicateFileException(archivePath, duplicate, file); |
| } |
| |
| mAddedFiles.put(archivePath, file); |
| mBuilder.writeFile(file, archivePath); |
| } |
| |
| /** |
| * Processes a {@link File} that could be an APK {@link File}, or a folder containing |
| * java resources. |
| * |
| * @param file the {@link File} to process. |
| * @param path the relative path of this file to the source folder. |
| * Can be <code>null</code> to identify a root file. |
| * @throws IOException |
| * @throws DuplicateFileException if a file conflicts with another already added |
| * to the APK at the same location inside the APK archive. |
| */ |
| private void processFileForResource(File file, String path) |
| throws IOException, DuplicateFileException { |
| if (file.isDirectory()) { |
| // a directory? we check it |
| if (checkFolderForPackaging(file.getName())) { |
| // if it's valid, we append its name to the current path. |
| if (path == null) { |
| path = file.getName(); |
| } else { |
| path = path + "/" + file.getName(); |
| } |
| |
| // and process its content. |
| File[] files = file.listFiles(); |
| for (File contentFile : files) { |
| processFileForResource(contentFile, path); |
| } |
| } |
| } else { |
| // a file? we check it to make sure it should be added |
| if (checkFileForPackaging(file.getName())) { |
| // we append its name to the current path |
| if (path == null) { |
| path = file.getName(); |
| } else { |
| path = path + "/" + file.getName(); |
| } |
| |
| // and add it to the apk |
| doAddFile(file, path); |
| } |
| } |
| } |
| |
| /** |
| * Checks if the given path in the APK archive has not already been used and if it has been, |
| * then returns a {@link File} object for the source of the duplicate |
| * @param archivePath the archive path to test. |
| * @return A File object of either a file at the same location or an archive that contains a |
| * file that was put at the same location. |
| */ |
| private File checkFileForDuplicate(String archivePath) { |
| return mAddedFiles.get(archivePath); |
| } |
| |
| /** |
| * Checks an output {@link File} object. |
| * This checks the following: |
| * - the file is not an existing directory. |
| * - if the file exists, that it can be modified. |
| * - if it doesn't exists, that a new file can be created. |
| * @param file the File to check |
| * @throws ApkCreationException If the check fails |
| */ |
| private void checkOutputFile(File file) throws ApkCreationException { |
| if (file.isDirectory()) { |
| throw new ApkCreationException("%s is a directory!", file); |
| } |
| |
| if (file.exists()) { // will be a file in this case. |
| if (file.canWrite() == false) { |
| throw new ApkCreationException("Cannot write %s", file); |
| } |
| } else { |
| try { |
| if (file.createNewFile() == false) { |
| throw new ApkCreationException("Failed to create %s", file); |
| } |
| } catch (IOException e) { |
| throw new ApkCreationException( |
| "Failed to create '%1$ss': %2$s", file, e.getMessage()); |
| } |
| } |
| } |
| |
| /** |
| * Checks an input {@link File} object. |
| * This checks the following: |
| * - the file is not an existing directory. |
| * - that the file exists (if <var>throwIfDoesntExist</var> is <code>false</code>) and can |
| * be read. |
| * @param file the File to check |
| * @throws FileNotFoundException if the file is not here. |
| * @throws ApkCreationException If the file is a folder or a file that cannot be read. |
| */ |
| private static void checkInputFile(File file) throws FileNotFoundException, ApkCreationException { |
| if (file.isDirectory()) { |
| throw new ApkCreationException("%s is a directory!", file); |
| } |
| |
| if (file.exists()) { |
| if (file.canRead() == false) { |
| throw new ApkCreationException("Cannot read %s", file); |
| } |
| } else { |
| throw new FileNotFoundException(String.format("%s does not exist", file)); |
| } |
| } |
| |
| public static String getDebugKeystore() throws ApkCreationException { |
| try { |
| return DebugKeyProvider.getDefaultKeyStoreOsPath(); |
| } catch (Exception e) { |
| throw new ApkCreationException(e, e.getMessage()); |
| } |
| } |
| |
| /** |
| * Checks whether a folder and its content is valid for packaging into the .apk as |
| * standard Java resource. |
| * @param folderName the name of the folder. |
| */ |
| public static boolean checkFolderForPackaging(String folderName) { |
| return folderName.equalsIgnoreCase("CVS") == false && |
| folderName.equalsIgnoreCase(".svn") == false && |
| folderName.equalsIgnoreCase("SCCS") == false && |
| folderName.equalsIgnoreCase("META-INF") == false && |
| folderName.startsWith("_") == false; |
| } |
| |
| /** |
| * Checks a file to make sure it should be packaged as standard resources. |
| * @param fileName the name of the file (including extension) |
| * @return true if the file should be packaged as standard java resources. |
| */ |
| public static boolean checkFileForPackaging(String fileName) { |
| String[] fileSegments = fileName.split("\\."); |
| String fileExt = ""; |
| if (fileSegments.length > 1) { |
| fileExt = fileSegments[fileSegments.length-1]; |
| } |
| |
| return checkFileForPackaging(fileName, fileExt); |
| } |
| |
| /** |
| * Checks a file to make sure it should be packaged as standard resources. |
| * @param fileName the name of the file (including extension) |
| * @param extension the extension of the file (excluding '.') |
| * @return true if the file should be packaged as standard java resources. |
| */ |
| public static boolean checkFileForPackaging(String fileName, String extension) { |
| // ignore hidden files and backup files |
| if (fileName.charAt(0) == '.' || fileName.charAt(fileName.length()-1) == '~') { |
| return false; |
| } |
| |
| return "aidl".equalsIgnoreCase(extension) == false && // Aidl files |
| "rs".equalsIgnoreCase(extension) == false && // RenderScript files |
| "rsh".equalsIgnoreCase(extension) == false && // RenderScript header files |
| "d".equalsIgnoreCase(extension) == false && // Dependency files |
| "java".equalsIgnoreCase(extension) == false && // Java files |
| "scala".equalsIgnoreCase(extension) == false && // Scala files |
| "class".equalsIgnoreCase(extension) == false && // Java class files |
| "scc".equalsIgnoreCase(extension) == false && // VisualSourceSafe |
| "swp".equalsIgnoreCase(extension) == false && // vi swap file |
| "thumbs.db".equalsIgnoreCase(fileName) == false && // image index file |
| "picasa.ini".equalsIgnoreCase(fileName) == false && // image index file |
| "package.html".equalsIgnoreCase(fileName) == false && // Javadoc |
| "overview.html".equalsIgnoreCase(fileName) == false; // Javadoc |
| } |
| } |