am a43be879: Merge change 4689 into donut

Merge commit 'a43be8798e54a208b4b06e0e30c7311d8711dd5d'

* commit 'a43be8798e54a208b4b06e0e30c7311d8711dd5d':
  Update the API of ApkBuilder to make it clearer what is stable and what isn't.
This commit is contained in:
Android (Google) Code Review
2009-06-19 11:17:20 -07:00
committed by The Android Open Source Project
3 changed files with 521 additions and 430 deletions

View File

@@ -16,8 +16,9 @@
package com.android.ant;
import com.android.apkbuilder.ApkBuilder;
import com.android.apkbuilder.ApkBuilder.ApkFile;
import com.android.apkbuilder.ApkBuilder.ApkCreationException;
import com.android.apkbuilder.internal.ApkBuilderImpl;
import com.android.apkbuilder.internal.ApkBuilderImpl.ApkFile;
import com.android.sdklib.internal.project.ApkConfigurationHelper;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
@@ -37,7 +38,7 @@ import java.util.Set;
import java.util.Map.Entry;
public class ApkBuilderTask extends Task {
/**
* Class to represent nested elements. Since they all have only one attribute ('path'), the
* same class can be used for all the nested elements (zip, file, sourcefolder, jarfolder,
@@ -45,7 +46,7 @@ public class ApkBuilderTask extends Task {
*/
public final static class Value extends ProjectComponent {
String mPath;
/**
* Sets the value of the "path" attribute.
* @param path the value.
@@ -59,7 +60,7 @@ public class ApkBuilderTask extends Task {
private String mBaseName;
private boolean mVerbose = false;
private boolean mSigned = true;
private final ArrayList<Value> mZipList = new ArrayList<Value>();
private final ArrayList<Value> mFileList = new ArrayList<Value>();
private final ArrayList<Value> mSourceList = new ArrayList<Value>();
@@ -79,7 +80,7 @@ public class ApkBuilderTask extends Task {
public void setOutfolder(Path outFolder) {
mOutFolder = outFolder.toString();
}
/**
* Sets the value of the "basename" attribute.
* @param baseName the value.
@@ -87,7 +88,7 @@ public class ApkBuilderTask extends Task {
public void setBasename(String baseName) {
mBaseName = baseName;
}
/**
* Sets the value of the "verbose" attribute.
* @param verbose the value.
@@ -95,7 +96,7 @@ public class ApkBuilderTask extends Task {
public void setVerbose(boolean verbose) {
mVerbose = verbose;
}
/**
* Sets the value of the "signed" attribute.
* @param signed the value.
@@ -103,7 +104,7 @@ public class ApkBuilderTask extends Task {
public void setSigned(boolean signed) {
mSigned = signed;
}
/**
* Returns an object representing a nested <var>zip</var> element.
*/
@@ -112,7 +113,7 @@ public class ApkBuilderTask extends Task {
mZipList.add(zip);
return zip;
}
/**
* Returns an object representing a nested <var>file</var> element.
*/
@@ -139,7 +140,7 @@ public class ApkBuilderTask extends Task {
mJarList.add(file);
return file;
}
/**
* Returns an object representing a nested <var>nativefolder</var> element.
*/
@@ -148,64 +149,64 @@ public class ApkBuilderTask extends Task {
mNativeList.add(file);
return file;
}
@Override
public void execute() throws BuildException {
Project taskProject = getProject();
ApkBuilder apkBuilder = new ApkBuilder();
ApkBuilderImpl apkBuilder = new ApkBuilderImpl();
apkBuilder.setVerbose(mVerbose);
apkBuilder.setSignedPackage(mSigned);
try {
// setup the list of everything that needs to go in the archive.
// go through the list of zip files to add. This will not include
// the resource package, which is handled separaly for each apk to create.
for (Value v : mZipList) {
FileInputStream input = new FileInputStream(v.mPath);
mZipArchives.add(input);
}
// now go through the list of file to directly add the to the list.
for (Value v : mFileList) {
mArchiveFiles.add(ApkBuilder.getInputFile(v.mPath));
mArchiveFiles.add(ApkBuilderImpl.getInputFile(v.mPath));
}
// now go through the list of file to directly add the to the list.
for (Value v : mSourceList) {
ApkBuilder.processSourceFolderForResource(v.mPath, mJavaResources);
ApkBuilderImpl.processSourceFolderForResource(v.mPath, mJavaResources);
}
// now go through the list of jar folders.
for (Value v : mJarList) {
ApkBuilder.processJarFolder(v.mPath, mResourcesJars);
ApkBuilderImpl.processJarFolder(v.mPath, mResourcesJars);
}
// now the native lib folder.
for (Value v : mNativeList) {
String parameter = v.mPath;
File f = new File(parameter);
// compute the offset to get the relative path
int offset = parameter.length();
if (parameter.endsWith(File.separator) == false) {
offset++;
}
ApkBuilder.processNativeFolder(offset, f, mNativeLibraries);
ApkBuilderImpl.processNativeFolder(offset, f, mNativeLibraries);
}
// first do a full resource package
createApk(apkBuilder, null /*configName*/, null /*resourceFilter*/);
// now see if we need to create file with filtered resources.
// Get the project base directory.
File baseDir = taskProject.getBaseDir();
ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(),
PropertyType.DEFAULT);
Map<String, String> apkConfigs = ApkConfigurationHelper.getConfigs(properties);
if (apkConfigs.size() > 0) {
Set<Entry<String, String>> entrySet = apkConfigs.entrySet();
@@ -217,20 +218,23 @@ public class ApkBuilderTask extends Task {
throw new BuildException(e);
} catch (IllegalArgumentException e) {
throw new BuildException(e);
} catch (ApkCreationException e) {
throw new BuildException(e);
}
}
/**
* Creates an application package.
* @param apkBuilder
* @param apkBuilder
* @param configName the name of the filter config. Can be null in which case a full resource
* package will be generated.
* @param resourceFilter the resource configuration filter to pass to aapt (if configName is
* non null)
* @throws FileNotFoundException
* @throws FileNotFoundException
* @throws ApkCreationException
*/
private void createApk(ApkBuilder apkBuilder, String configName, String resourceFilter)
throws FileNotFoundException {
private void createApk(ApkBuilderImpl apkBuilder, String configName, String resourceFilter)
throws FileNotFoundException, ApkCreationException {
// All the files to be included in the archive have already been prep'ed up, except
// the resource package.
// figure out its name.
@@ -240,20 +244,20 @@ public class ApkBuilderTask extends Task {
} else {
filename = mBaseName + ".ap_";
}
// now we add it to the list of zip archive (it's just a zip file).
// it's used as a zip archive input
FileInputStream resoucePackageZipFile = new FileInputStream(new File(mOutFolder, filename));
mZipArchives.add(resoucePackageZipFile);
// prepare the filename to generate. Same thing as the resource file.
if (configName != null && resourceFilter != null) {
filename = mBaseName + "-" + configName;
} else {
filename = mBaseName;
}
if (mSigned) {
filename = filename + "-debug.apk";
} else {
@@ -279,13 +283,13 @@ public class ApkBuilderTask extends Task {
filename, resourceFilter));
}
}
File f = new File(mOutFolder, filename);
// and generate the apk
apkBuilder.createPackage(f.getAbsoluteFile(), mZipArchives,
mArchiveFiles, mJavaResources, mResourcesJars, mNativeLibraries);
// we are done. We need to remove the resource package from the list of zip archives
// in case we have another apk to generate.
mZipArchives.remove(resoucePackageZipFile);

View File

@@ -16,419 +16,69 @@
package com.android.apkbuilder;
import com.android.jarutils.DebugKeyProvider;
import com.android.jarutils.JavaResourceFilter;
import com.android.jarutils.SignedJarBuilder;
import com.android.jarutils.DebugKeyProvider.KeytoolException;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.apkbuilder.internal.ApkBuilderImpl;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.regex.Pattern;
/**
* Command line APK builder with signing support.
*/
public final class ApkBuilder {
private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
Pattern.CASE_INSENSITIVE);
private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
Pattern.CASE_INSENSITIVE);
private final static String NATIVE_LIB_ROOT = "lib/";
/**
* A File to be added to the APK archive.
* <p/>This includes the {@link File} representing the file and its path in the archive.
*/
public final static class ApkFile {
String archivePath;
File file;
public final static class WrongOptionException extends Exception {
private static final long serialVersionUID = 1L;
ApkFile(File file, String path) {
this.file = file;
this.archivePath = path;
public WrongOptionException(String message) {
super(message);
}
}
private JavaResourceFilter mResourceFilter = new JavaResourceFilter();
private boolean mVerbose = false;
private boolean mSignedPackage = true;
/** the optional type of the debug keystore. If <code>null</code>, the default */
private String mStoreType = null;
public final static class ApkCreationException extends Exception {
private static final long serialVersionUID = 1L;
public ApkCreationException(String message) {
super(message);
}
public ApkCreationException(Throwable throwable) {
super(throwable);
}
}
/**
* @param args
* Main method. This is meant to be called from the command line through an exec.
* <p/>WARNING: this will call {@link System#exit(int)} if anything goes wrong.
* @param args command line arguments.
*/
public static void main(String[] args) {
new ApkBuilder().run(args);
}
public void setVerbose(boolean verbose) {
mVerbose = verbose;
}
public void setSignedPackage(boolean signedPackage) {
mSignedPackage = signedPackage;
}
private void run(String[] args) {
if (args.length < 1) {
printUsageAndQuit();
}
try {
// read the first args that should be a file path
File outFile = getOutFile(args[0]);
ArrayList<FileInputStream> zipArchives = new ArrayList<FileInputStream>();
ArrayList<File> archiveFiles = new ArrayList<File>();
ArrayList<ApkFile> javaResources = new ArrayList<ApkFile>();
ArrayList<FileInputStream> resourcesJars = new ArrayList<FileInputStream>();
ArrayList<ApkFile> nativeLibraries = new ArrayList<ApkFile>();
int index = 1;
do {
String argument = args[index++];
if ("-v".equals(argument)) {
mVerbose = true;
} else if ("-u".equals(argument)) {
mSignedPackage = false;
} else if ("-z".equals(argument)) {
// quick check on the next argument.
if (index == args.length) printUsageAndQuit();
try {
FileInputStream input = new FileInputStream(args[index++]);
zipArchives.add(input);
} catch (FileNotFoundException e) {
printAndExit(e.getMessage());
}
} else if ("-f". equals(argument)) {
// quick check on the next argument.
if (index == args.length) printUsageAndQuit();
archiveFiles.add(getInputFile(args[index++]));
} else if ("-rf". equals(argument)) {
// quick check on the next argument.
if (index == args.length) printUsageAndQuit();
processSourceFolderForResource(args[index++], javaResources);
} else if ("-rj". equals(argument)) {
// quick check on the next argument.
if (index == args.length) printUsageAndQuit();
processJarFolder(args[index++], resourcesJars);
} else if ("-nf".equals(argument)) {
// quick check on the next argument.
if (index == args.length) printUsageAndQuit();
String parameter = args[index++];
File f = new File(parameter);
// compute the offset to get the relative path
int offset = parameter.length();
if (parameter.endsWith(File.separator) == false) {
offset++;
}
processNativeFolder(offset, f, nativeLibraries);
} else if ("-storetype".equals(argument)) {
// quick check on the next argument.
if (index == args.length) printUsageAndQuit();
mStoreType = args[index++];
} else {
printAndExit("Unknown argument: " + argument);
}
} while (index < args.length);
createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
nativeLibraries);
} catch (IllegalArgumentException e) {
printAndExit(e.getMessage());
new ApkBuilderImpl().run(args);
} catch (WrongOptionException e) {
printUsageAndQuit();
} catch (FileNotFoundException e) {
printAndExit(e.getMessage());
}
}
private File getOutFile(String filepath) {
File f = new File(filepath);
if (f.isDirectory()) {
printAndExit(filepath + " is a directory!");
}
if (f.exists()) { // will be a file in this case.
if (f.canWrite() == false) {
printAndExit("Cannot write " + filepath);
}
} else {
try {
if (f.createNewFile() == false) {
printAndExit("Failed to create " + filepath);
}
} catch (IOException e) {
printAndExit("Failed to create '" + filepath + "' : " + e.getMessage());
}
}
return f;
}
public static File getInputFile(String filepath) throws IllegalArgumentException {
File f = new File(filepath);
if (f.isDirectory()) {
throw new IllegalArgumentException(filepath + " is a directory!");
}
if (f.exists()) {
if (f.canRead() == false) {
throw new IllegalArgumentException("Cannot read " + filepath);
}
} else {
throw new IllegalArgumentException(filepath + " does not exists!");
}
return f;
}
/**
* Processes a source folder and adds its java resources to a given list of {@link ApkFile}.
* @param folderPath the path to the source folder.
* @param javaResources the list of {@link ApkFile} to fill.
*/
public static void processSourceFolderForResource(String folderPath,
ArrayList<ApkFile> javaResources) {
File folder = new File(folderPath);
if (folder.isDirectory()) {
// file is a directory, process its content.
File[] files = folder.listFiles();
for (File file : files) {
processFileForResource(file, null, javaResources);
}
} else {
// not a directory? output error and quit.
if (folder.exists()) {
throw new IllegalArgumentException(folderPath + " is not a folder!");
} else {
throw new IllegalArgumentException(folderPath + " does not exist!");
}
}
}
public static void processJarFolder(String parameter, ArrayList<FileInputStream> resourcesJars)
throws FileNotFoundException {
File f = new File(parameter);
if (f.isDirectory()) {
String[] files = f.list(new FilenameFilter() {
public boolean accept(File dir, String name) {
return PATTERN_JAR_EXT.matcher(name).matches();
}
});
for (String file : files) {
String path = f.getAbsolutePath() + File.separator + file;
FileInputStream input = new FileInputStream(path);
resourcesJars.add(input);
}
} else {
FileInputStream input = new FileInputStream(parameter);
resourcesJars.add(input);
}
}
/**
* Processes a {@link File} that could be a {@link ApkFile}, 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.
* @param javaResources the list of {@link ApkFile} object to fill.
*/
private static void processFileForResource(File file, String path,
ArrayList<ApkFile> javaResources) {
if (file.isDirectory()) {
// a directory? we check it
if (JavaResourceFilter.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, javaResources);
}
}
} else {
// a file? we check it
if (JavaResourceFilter.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 list.
javaResources.add(new ApkFile(file, path));
}
}
}
/**
* Process a {@link File} for native library inclusion.
* @param offset the length of the root folder (used to compute relative path)
* @param f the {@link File} to process
* @param nativeLibraries the array to add native libraries.
*/
public static void processNativeFolder(int offset, File f, ArrayList<ApkFile> nativeLibraries) {
if (f.isDirectory()) {
File[] children = f.listFiles();
if (children != null) {
for (File child : children) {
processNativeFolder(offset, child, nativeLibraries);
}
}
} else if (f.isFile()) {
if (PATTERN_NATIVELIB_EXT.matcher(f.getName()).matches()) {
String path = NATIVE_LIB_ROOT +
f.getAbsolutePath().substring(offset).replace('\\', '/');
nativeLibraries.add(new ApkFile(f, path));
}
} catch (ApkCreationException e) {
printAndExit(e.getMessage());
}
}
/**
* Creates the application package
* @param outFile
* @param zipArchives
* @param resourcesJars
* @param files
* @param javaResources
* keystore type of the Java VM is used.
* API entry point similar to the {@link #main(String[])} method.
* <p/>Unlike {@link #main(String[])}, this will not call {@link System#exit(int)} and instead
* will throw exceptions.
* @param args command line arguments.
* @throws WrongOptionException if the command line arguments are incorrect.
* @throws FileNotFoundException if a required file was not found.
* @throws ApkCreationException if an error happened during the creation of the APK.
*/
public void createPackage(File outFile, ArrayList<FileInputStream> zipArchives,
ArrayList<File> files, ArrayList<ApkFile> javaResources,
ArrayList<FileInputStream> resourcesJars, ArrayList<ApkFile> nativeLibraries) {
// get the debug key
try {
SignedJarBuilder builder;
if (mSignedPackage) {
System.err.println(String.format("Using keystore: %s",
DebugKeyProvider.getDefaultKeyStoreOsPath()));
DebugKeyProvider keyProvider = new DebugKeyProvider(
null /* osKeyPath: use default */,
mStoreType, null /* IKeyGenOutput */);
PrivateKey key = keyProvider.getDebugKey();
X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();
if (key == null) {
throw new IllegalArgumentException("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 IllegalArgumentException("Debug Certificate expired on " +
DateFormat.getInstance().format(certificate.getNotAfter()));
}
builder = new SignedJarBuilder(
new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
certificate);
} else {
builder = new SignedJarBuilder(
new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
null /* key */, null /* certificate */);
}
// add the archives
for (FileInputStream input : zipArchives) {
builder.writeZip(input, null /* filter */);
}
// add the single files
for (File input : files) {
// always put the file at the root of the archive in this case
builder.writeFile(input, input.getName());
if (mVerbose) {
System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
input.getName()));
}
}
// add the java resource from the source folders.
for (ApkFile resource : javaResources) {
builder.writeFile(resource.file, resource.archivePath);
if (mVerbose) {
System.err.println(String.format("%1$s => %2$s",
resource.file.getAbsolutePath(), resource.archivePath));
}
}
// add the java resource from jar files.
for (FileInputStream input : resourcesJars) {
builder.writeZip(input, mResourceFilter);
}
// add the native files
for (ApkFile file : nativeLibraries) {
builder.writeFile(file.file, file.archivePath);
if (mVerbose) {
System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
file.archivePath));
}
}
// close and sign the application package.
builder.close();
} catch (KeytoolException e) {
if (e.getJavaHome() == null) {
throw new IllegalArgumentException(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());
} else {
throw new IllegalArgumentException(e.getMessage() +
"\nJAVA_HOME is set to: " + e.getJavaHome() +
"\nUpdate it if necessary, or manually execute the following command:\n" +
e.getCommandLine());
}
} catch (AndroidLocationException e) {
throw new IllegalArgumentException(e);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
public static void createApk(String[] args) throws FileNotFoundException, WrongOptionException,
ApkCreationException {
new ApkBuilderImpl().run(args);
}
private void printUsageAndQuit() {
private static void printUsageAndQuit() {
// 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789
System.err.println("A command line tool to package an Android application from various sources.");
System.err.println("Usage: apkbuilder <out archive> [-v][-u][-storetype STORE_TYPE] [-z inputzip]");
@@ -455,11 +105,11 @@ public final class ApkBuilder {
System.err.println("");
System.err.println(" -nf Followed by the root folder containing native libraries to");
System.err.println(" include in the application package.");
System.exit(1);
}
private void printAndExit(String... messages) {
private static void printAndExit(String... messages) {
for (String message : messages) {
System.err.println(message);
}

View File

@@ -0,0 +1,437 @@
/*
* Copyright (C) 2008 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.apkbuilder.internal;
import com.android.apkbuilder.ApkBuilder.WrongOptionException;
import com.android.apkbuilder.ApkBuilder.ApkCreationException;
import com.android.jarutils.DebugKeyProvider;
import com.android.jarutils.JavaResourceFilter;
import com.android.jarutils.SignedJarBuilder;
import com.android.jarutils.DebugKeyProvider.KeytoolException;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.regex.Pattern;
/**
* Command line APK builder with signing support.
*/
public final class ApkBuilderImpl {
private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
Pattern.CASE_INSENSITIVE);
private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
Pattern.CASE_INSENSITIVE);
private final static String NATIVE_LIB_ROOT = "lib/";
/**
* A File to be added to the APK archive.
* <p/>This includes the {@link File} representing the file and its path in the archive.
*/
public final static class ApkFile {
String archivePath;
File file;
ApkFile(File file, String path) {
this.file = file;
this.archivePath = path;
}
}
private JavaResourceFilter mResourceFilter = new JavaResourceFilter();
private boolean mVerbose = false;
private boolean mSignedPackage = true;
/** the optional type of the debug keystore. If <code>null</code>, the default */
private String mStoreType = null;
public void setVerbose(boolean verbose) {
mVerbose = verbose;
}
public void setSignedPackage(boolean signedPackage) {
mSignedPackage = signedPackage;
}
public void run(String[] args) throws WrongOptionException, FileNotFoundException,
ApkCreationException {
if (args.length < 1) {
throw new WrongOptionException("No options specified");
}
// read the first args that should be a file path
File outFile = getOutFile(args[0]);
ArrayList<FileInputStream> zipArchives = new ArrayList<FileInputStream>();
ArrayList<File> archiveFiles = new ArrayList<File>();
ArrayList<ApkFile> javaResources = new ArrayList<ApkFile>();
ArrayList<FileInputStream> resourcesJars = new ArrayList<FileInputStream>();
ArrayList<ApkFile> nativeLibraries = new ArrayList<ApkFile>();
int index = 1;
do {
String argument = args[index++];
if ("-v".equals(argument)) {
mVerbose = true;
} else if ("-u".equals(argument)) {
mSignedPackage = false;
} else if ("-z".equals(argument)) {
// quick check on the next argument.
if (index == args.length) {
throw new WrongOptionException("Missing value for -z");
}
try {
FileInputStream input = new FileInputStream(args[index++]);
zipArchives.add(input);
} catch (FileNotFoundException e) {
throw new ApkCreationException("-z file is not found");
}
} else if ("-f". equals(argument)) {
// quick check on the next argument.
if (index == args.length) {
throw new WrongOptionException("Missing value for -f");
}
archiveFiles.add(getInputFile(args[index++]));
} else if ("-rf". equals(argument)) {
// quick check on the next argument.
if (index == args.length) {
throw new WrongOptionException("Missing value for -rf");
}
processSourceFolderForResource(args[index++], javaResources);
} else if ("-rj". equals(argument)) {
// quick check on the next argument.
if (index == args.length) {
throw new WrongOptionException("Missing value for -rj");
}
processJarFolder(args[index++], resourcesJars);
} else if ("-nf".equals(argument)) {
// quick check on the next argument.
if (index == args.length) {
throw new WrongOptionException("Missing value for -nf");
}
String parameter = args[index++];
File f = new File(parameter);
// compute the offset to get the relative path
int offset = parameter.length();
if (parameter.endsWith(File.separator) == false) {
offset++;
}
processNativeFolder(offset, f, nativeLibraries);
} else if ("-storetype".equals(argument)) {
// quick check on the next argument.
if (index == args.length) {
throw new WrongOptionException("Missing value for -storetype");
}
mStoreType = args[index++];
} else {
throw new WrongOptionException("Unknown argument: " + argument);
}
} while (index < args.length);
createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
nativeLibraries);
}
private File getOutFile(String filepath) throws ApkCreationException {
File f = new File(filepath);
if (f.isDirectory()) {
throw new ApkCreationException(filepath + " is a directory!");
}
if (f.exists()) { // will be a file in this case.
if (f.canWrite() == false) {
throw new ApkCreationException("Cannot write " + filepath);
}
} else {
try {
if (f.createNewFile() == false) {
throw new ApkCreationException("Failed to create " + filepath);
}
} catch (IOException e) {
throw new ApkCreationException(
"Failed to create '" + filepath + "' : " + e.getMessage());
}
}
return f;
}
public static File getInputFile(String filepath) throws ApkCreationException {
File f = new File(filepath);
if (f.isDirectory()) {
throw new ApkCreationException(filepath + " is a directory!");
}
if (f.exists()) {
if (f.canRead() == false) {
throw new ApkCreationException("Cannot read " + filepath);
}
} else {
throw new ApkCreationException(filepath + " does not exists!");
}
return f;
}
/**
* Processes a source folder and adds its java resources to a given list of {@link ApkFile}.
* @param folderPath the path to the source folder.
* @param javaResources the list of {@link ApkFile} to fill.
* @throws ApkCreationException
*/
public static void processSourceFolderForResource(String folderPath,
ArrayList<ApkFile> javaResources) throws ApkCreationException {
File folder = new File(folderPath);
if (folder.isDirectory()) {
// file is a directory, process its content.
File[] files = folder.listFiles();
for (File file : files) {
processFileForResource(file, null, javaResources);
}
} else {
// not a directory? output error and quit.
if (folder.exists()) {
throw new ApkCreationException(folderPath + " is not a folder!");
} else {
throw new ApkCreationException(folderPath + " does not exist!");
}
}
}
public static void processJarFolder(String parameter, Collection<FileInputStream> resourcesJars)
throws FileNotFoundException {
File f = new File(parameter);
if (f.isDirectory()) {
String[] files = f.list(new FilenameFilter() {
public boolean accept(File dir, String name) {
return PATTERN_JAR_EXT.matcher(name).matches();
}
});
for (String file : files) {
String path = f.getAbsolutePath() + File.separator + file;
FileInputStream input = new FileInputStream(path);
resourcesJars.add(input);
}
} else {
FileInputStream input = new FileInputStream(parameter);
resourcesJars.add(input);
}
}
/**
* Processes a {@link File} that could be a {@link ApkFile}, 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.
* @param javaResources the Collection of {@link ApkFile} object to fill.
*/
private static void processFileForResource(File file, String path,
Collection<ApkFile> javaResources) {
if (file.isDirectory()) {
// a directory? we check it
if (JavaResourceFilter.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, javaResources);
}
}
} else {
// a file? we check it
if (JavaResourceFilter.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 list.
javaResources.add(new ApkFile(file, path));
}
}
}
/**
* Process a {@link File} for native library inclusion.
* @param offset the length of the root folder (used to compute relative path)
* @param f the {@link File} to process
* @param nativeLibraries the collection to add native libraries to.
*/
public static void processNativeFolder(int offset, File f,
Collection<ApkFile> nativeLibraries) {
if (f.isDirectory()) {
File[] children = f.listFiles();
if (children != null) {
for (File child : children) {
processNativeFolder(offset, child, nativeLibraries);
}
}
} else if (f.isFile()) {
if (PATTERN_NATIVELIB_EXT.matcher(f.getName()).matches()) {
String path = NATIVE_LIB_ROOT +
f.getAbsolutePath().substring(offset).replace('\\', '/');
nativeLibraries.add(new ApkFile(f, path));
}
}
}
/**
* Creates the application package
* @param outFile the package file to create
* @param zipArchives the list of zip archive
* @param files the list of files to include in the archive
* @param javaResources the list of java resources from the source folders.
* @param resourcesJars the list of jar files from which to take java resources
* @throws ApkCreationException
*/
public void createPackage(File outFile, Iterable<? extends FileInputStream> zipArchives,
Iterable<? extends File> files, Iterable<? extends ApkFile> javaResources,
Iterable<? extends FileInputStream> resourcesJars,
Iterable<? extends ApkFile> nativeLibraries) throws ApkCreationException {
// get the debug key
try {
SignedJarBuilder builder;
if (mSignedPackage) {
System.err.println(String.format("Using keystore: %s",
DebugKeyProvider.getDefaultKeyStoreOsPath()));
DebugKeyProvider keyProvider = new DebugKeyProvider(
null /* osKeyPath: use default */,
mStoreType, null /* IKeyGenOutput */);
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()));
}
builder = new SignedJarBuilder(
new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
certificate);
} else {
builder = new SignedJarBuilder(
new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
null /* key */, null /* certificate */);
}
// add the archives
for (FileInputStream input : zipArchives) {
builder.writeZip(input, null /* filter */);
}
// add the single files
for (File input : files) {
// always put the file at the root of the archive in this case
builder.writeFile(input, input.getName());
if (mVerbose) {
System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
input.getName()));
}
}
// add the java resource from the source folders.
for (ApkFile resource : javaResources) {
builder.writeFile(resource.file, resource.archivePath);
if (mVerbose) {
System.err.println(String.format("%1$s => %2$s",
resource.file.getAbsolutePath(), resource.archivePath));
}
}
// add the java resource from jar files.
for (FileInputStream input : resourcesJars) {
builder.writeZip(input, mResourceFilter);
}
// add the native files
for (ApkFile file : nativeLibraries) {
builder.writeFile(file.file, file.archivePath);
if (mVerbose) {
System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
file.archivePath));
}
}
// close and sign the application package.
builder.close();
} 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());
} 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());
}
} catch (AndroidLocationException e) {
throw new ApkCreationException(e);
} catch (Exception e) {
throw new ApkCreationException(e);
}
}
}