/*
* 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.SdkConstants;
import com.android.sdklib.internal.build.DebugKeyProvider;
import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput;
import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
import com.android.sdklib.internal.build.SignedJarBuilder;
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);
private final static Pattern PATTERN_BITCODELIB_EXT = Pattern.compile("^.+\\.bc$",
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;
}
@Override
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.
*
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 mNativeLibs = new ArrayList();
private boolean mNativeLibsConflict = false;
private File mInputFile;
@Override
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") || archivePath.endsWith(".bc")) {
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 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 mAddedFiles = new HashMap();
/**
* 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 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 mLibs;
public final boolean mNativeLibsConflict;
private JarStatusImpl(List libs, boolean nativeLibsConflict) {
mLibs = libs;
mNativeLibsConflict = nativeLibsConflict;
}
@Override
public List getNativeLibs() {
return mLibs;
}
@Override
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() {
@Override
public void out(String message) {
verboseStream.println(message);
}
@Override
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) {
if (mBuilder != null) {
mBuilder.cleanUp();
}
throw e;
} catch (Exception e) {
if (mBuilder != null) {
mBuilder.cleanUp();
}
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.
*/
@Override
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) {
mBuilder.cleanUp();
throw e;
} catch (Exception e) {
mBuilder.cleanUp();
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) {
mBuilder.cleanUp();
throw e;
} catch (Exception e) {
mBuilder.cleanUp();
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) {
mBuilder.cleanUp();
throw e;
} catch (Exception e) {
mBuilder.cleanUp();
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");
}
addSourceFolder(this, sourceFolder);
}
/**
* Adds the resources from a source folder to a given {@link IArchiveBuilder}
* @param sourceFolder the source folder.
* @throws ApkCreationException if an error occurred
* @throws DuplicateFileException if a file conflicts with another already added to the APK
* at the same location inside the APK archive.
*/
public static void addSourceFolder(IArchiveBuilder builder, File sourceFolder)
throws ApkCreationException, DuplicateFileException {
if (sourceFolder.isDirectory()) {
try {
// file is a directory, process its content.
File[] files = sourceFolder.listFiles();
for (File file : files) {
processFileForResource(builder, 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() ||
PATTERN_BITCODELIB_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) {
mBuilder.cleanUp();
throw new ApkCreationException(e, "Failed to add %s", lib);
}
}
}
}
}
}
}
}
public void addNativeLibraries(List 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) {
mBuilder.cleanUp();
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 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 files = new ArrayList();
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() ||
PATTERN_BITCODELIB_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");
} finally {
mBuilder.cleanUp();
}
}
/**
* 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 null
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.
* @throws SealedApkException if the APK is already sealed.
* @throws ApkCreationException if an error occurred
*/
private static void processFileForResource(IArchiveBuilder builder, File file, String path)
throws IOException, DuplicateFileException, ApkCreationException, SealedApkException {
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(builder, 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
builder.addFile(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 throwIfDoesntExist is false
) 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
}
}