/*
* 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.ant;
import com.android.sdklib.internal.export.ApkData;
import com.android.sdklib.internal.export.MultiApkExportHelper;
import com.android.sdklib.internal.export.ProjectConfig;
import com.android.sdklib.internal.export.MultiApkExportHelper.ExportException;
import com.android.sdklib.internal.export.MultiApkExportHelper.Target;
import com.android.sdklib.internal.project.ProjectProperties;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Input;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.taskdefs.SubAnt;
import org.apache.tools.ant.types.FileSet;
import org.xml.sax.InputSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
/**
* Multiple APK export task.
* This task is meant to replace {@link SetupTask} as the main setup/export task, importing
* the rules and generating the export for all projects.
*/
public class MultiApkExportTask extends Task {
private Target mTarget;
private XPathFactory mXPathFactory;
public void setTarget(String target) {
mTarget = Target.getTarget(target);
}
@Override
public void execute() throws BuildException {
Project antProject = getProject();
if (mTarget == null) {
throw new BuildException("'target' attribute not set.");
}
// get the SDK location
File sdk = TaskHelper.getSdkLocation(antProject);
// display SDK Tools revision
int toolsRevison = TaskHelper.getToolsRevision(sdk);
if (toolsRevison != -1) {
System.out.println("Android SDK Tools Revision " + toolsRevison);
}
String appPackage = getValidatedProperty(antProject, ProjectProperties.PROPERTY_PACKAGE);
System.out.println("Multi APK export for: " + appPackage);
String version = getValidatedProperty(antProject, ProjectProperties.PROPERTY_VERSIONCODE);
int versionCode;
try {
versionCode = Integer.parseInt(version);
} catch (NumberFormatException e) {
throw new BuildException("version value is not a valid integer.", e);
}
System.out.println("versionCode: " + version);
// get the list of projects
String projectList = getValidatedProperty(antProject, "projects");
File rootFolder = antProject.getBaseDir();
MultiApkExportHelper helper = new MultiApkExportHelper(rootFolder.getAbsolutePath(),
appPackage, versionCode, mTarget, System.out);
try {
if (mTarget == Target.CLEAN) {
// for a clean, we don't need the list of ApkData, we only need the list of
// projects
List<ProjectConfig> projects = helper.getProjects(projectList);
for (ProjectConfig projectConfig : projects) {
executeCleanSubAnt(antProject, projectConfig);
}
} else {
// checks whether the projects can be signed.
String value = antProject.getProperty(ProjectProperties.PROPERTY_KEY_STORE);
String keyStore = value != null && value.length() > 0 ? value : null;
value = antProject.getProperty(ProjectProperties.PROPERTY_KEY_ALIAS);
String keyAlias = value != null && value.length() > 0 ? value : null;
boolean canSign = keyStore != null && keyAlias != null;
List<ApkData> apks = helper.getApkData(projectList);
// some temp var used by the project loop
HashSet<String> compiledProject = new HashSet<String>();
mXPathFactory = XPathFactory.newInstance();
File exportProjectOutput = new File(
getValidatedProperty(antProject, AntConstants.PROP_OUT_ABS_DIR));
// if there's no error, and we can sign, prompt for the passwords.
String keyStorePassword = null;
String keyAliasPassword = null;
if (canSign) {
System.out.println("Found signing keystore and key alias. Need passwords.");
Input input = new Input();
input.setProject(antProject);
input.setAddproperty(AntConstants.PROP_KEY_STORE_PASSWORD);
input.setMessage(String.format("Please enter keystore password (store: %1$s):",
keyStore));
input.execute();
input = new Input();
input.setProject(antProject);
input.setAddproperty(AntConstants.PROP_KEY_ALIAS_PASSWORD);
input.setMessage(String.format("Please enter password for alias '%1$s':",
keyAlias));
input.execute();
// and now read the property so that they can be set into the sub ant task.
keyStorePassword = getValidatedProperty(antProject,
AntConstants.PROP_KEY_STORE_PASSWORD);
keyAliasPassword = getValidatedProperty(antProject,
AntConstants.PROP_KEY_ALIAS_PASSWORD);
}
for (ApkData apk : apks) {
Map<String, String> variantMap = apk.getSoftVariantMap();
if (variantMap.size() > 0) {
// if there are soft variants, only export those.
for (Entry<String, String> entry : variantMap.entrySet()) {
executeReleaseSubAnt(antProject, appPackage, versionCode, apk, entry,
exportProjectOutput, canSign, keyStore, keyAlias,
keyStorePassword, keyAliasPassword, compiledProject);
}
} else {
// do the full export.
executeReleaseSubAnt(antProject, appPackage, versionCode, apk, null,
exportProjectOutput, canSign, keyStore, keyAlias,
keyStorePassword, keyAliasPassword, compiledProject);
}
}
helper.writeLogs();
}
} catch (ExportException e) {
// we only want to have Ant display the message, not the stack trace, since
// we use Exceptions to report errors, so we build the BuildException only
// with the message and not the cause exception.
throw new BuildException(e.getMessage());
}
}
/**
* Creates and execute a clean sub ant task.
* @param antProject the current Ant project
* @param projectConfig the project to clean.
*/
private void executeCleanSubAnt(Project antProject, ProjectConfig projectConfig) {
String relativePath = projectConfig.getRelativePath();
// this output is prepended by "[android-export] " (17 chars), so we put 61 stars
System.out.println("\n*************************************************************");
System.out.println("Cleaning project: " + relativePath);
SubAnt subAnt = new SubAnt();
subAnt.setTarget(mTarget.getTarget());
subAnt.setProject(antProject);
File subProjectFolder = projectConfig.getProjectFolder();
FileSet fileSet = new FileSet();
fileSet.setProject(antProject);
fileSet.setDir(subProjectFolder);
fileSet.setIncludes("build.xml");
subAnt.addFileset(fileSet);
// TODO: send the verbose flag from the main build.xml to the subAnt project.
//subAnt.setVerbose(true);
// end of the output by this task. Everything that follows will be output
// by the subant.
System.out.println("Calling to project's Ant file...");
System.out.println("----------\n");
subAnt.execute();
}
/**
* Creates and executes a release sub ant task.
* @param antProject the current Ant project
* @param appPackage the application package string.
* @param versionCode the current version of the application
* @param apk the {@link ApkData} being exported.
* @param softVariant the soft variant being exported, or null, if this is a full export.
* @param exportProjectOutput the folder in which the files must be exported.
* @param canSign whether the application package can be signed. This is dependent on the
* availability of some required values.
* @param keyStore the path to the keystore for signing
* @param keyAlias the alias of the key to be used for signing
* @param keyStorePassword the password of the keystore for signing
* @param keyAliasPassword the password of the key alias for signing
* @param compiledProject a list of projects that have already been compiled.
*/
private void executeReleaseSubAnt(Project antProject, String appPackage, int versionCode,
ApkData apk, Entry<String, String> softVariant, File exportProjectOutput,
boolean canSign, String keyStore, String keyAlias,
String keyStorePassword, String keyAliasPassword, Set<String> compiledProject) {
String relativePath = apk.getProjectConfig().getRelativePath();
// this output is prepended by "[android-export] " (17 chars), so we put 61 stars
System.out.println("\n*************************************************************");
System.out.println("Exporting project: " + relativePath);
SubAnt subAnt = new SubAnt();
subAnt.setTarget(mTarget.getTarget());
subAnt.setProject(antProject);
File subProjectFolder = apk.getProjectConfig().getProjectFolder();
FileSet fileSet = new FileSet();
fileSet.setProject(antProject);
fileSet.setDir(subProjectFolder);
fileSet.setIncludes("build.xml");
subAnt.addFileset(fileSet);
// TODO: send the verbose flag from the main build.xml to the subAnt project.
//subAnt.setVerbose(true);
// only do the compilation part if it's the first time we export
// this project.
// (projects can be export multiple time if some properties are set up to
// generate more than one APK (for instance ABI split).
if (compiledProject.contains(relativePath) == false) {
compiledProject.add(relativePath);
} else {
addProp(subAnt, "do.not.compile", "true");
}
// set the version code, and filtering
int compositeVersionCode = apk.getCompositeVersionCode(versionCode);
addProp(subAnt, "version.code", Integer.toString(compositeVersionCode));
System.out.println("Composite versionCode: " + compositeVersionCode);
String abi = apk.getAbi();
if (abi != null) {
addProp(subAnt, "filter.abi", abi);
System.out.println("ABI Filter: " + abi);
}
// set the output file names/paths. Keep all the temporary files in the project
// folder, and only put the final file (which is different depending on whether
// the file can be signed) locally.
// read the base name from the build.xml file.
String name = null;
try {
File buildFile = new File(subProjectFolder, "build.xml");
XPath xPath = mXPathFactory.newXPath();
name = xPath.evaluate("/project/@name",
new InputSource(new FileInputStream(buildFile)));
} catch (XPathExpressionException e) {
throw new BuildException("Failed to read build.xml", e);
} catch (FileNotFoundException e) {
throw new BuildException("build.xml is missing.", e);
}
// override the resource pack file as well as the final name
String pkgName = name + "-" + apk.getBuildInfo();
String finalNameRoot = appPackage + "-" + compositeVersionCode;
if (softVariant != null) {
String tmp = "-" + softVariant.getKey();
pkgName += tmp;
finalNameRoot += tmp;
// set the resource filter.
addProp(subAnt, "aapt.resource.filter", softVariant.getValue());
System.out.println("res Filter: " + softVariant.getValue());
}
// set the resource pack file name.
addProp(subAnt, "resource.package.file.name", pkgName + ".ap_");
if (canSign) {
// set the properties for the password.
addProp(subAnt, ProjectProperties.PROPERTY_KEY_STORE, keyStore);
addProp(subAnt, ProjectProperties.PROPERTY_KEY_ALIAS, keyAlias);
addProp(subAnt, "key.store.password", keyStorePassword);
addProp(subAnt, "key.alias.password", keyAliasPassword);
// temporary file only get a filename change (still stored in the project
// bin folder).
addProp(subAnt, "out.unsigned.file.name",
name + "-" + apk.getBuildInfo() + "-unsigned.apk");
addProp(subAnt, "out.unaligned.file",
name + "-" + apk.getBuildInfo() + "-unaligned.apk");
// final file is stored locally with a name based on the package
String outputName = finalNameRoot + "-release.apk";
apk.setOutputName(softVariant != null ? softVariant.getKey() : null, outputName);
addProp(subAnt, "out.release.file",
new File(exportProjectOutput, outputName).getAbsolutePath());
} else {
// put some empty prop. This is to override possible ones defined in the
// project. The reason is that if there's more than one project, we don't
// want some to signed and some not to be (and we don't want each project
// to prompt for password.)
addProp(subAnt, ProjectProperties.PROPERTY_KEY_STORE, "");
addProp(subAnt, ProjectProperties.PROPERTY_KEY_ALIAS, "");
// final file is the unsigned version. It gets stored locally.
String outputName = finalNameRoot + "-unsigned.apk";
apk.setOutputName(softVariant != null ? softVariant.getKey() : null, outputName);
addProp(subAnt, "out.unsigned.file",
new File(exportProjectOutput, outputName).getAbsolutePath());
}
// end of the output by this task. Everything that follows will be output
// by the subant.
System.out.println("Calling to project's Ant file...");
System.out.println("----------\n");
subAnt.execute();
}
/**
* Gets, validates and returns a project property.
* The property must exist and be non empty.
* @param antProject the project
* @param name the name of the property to return.
* @return the property value always (cannot be null).
* @throws BuildException if the property is missing or not valid.
*/
private String getValidatedProperty(Project antProject, String name) {
String value = antProject.getProperty(name);
if (value == null || value.length() == 0) {
throw new BuildException(String.format("Property '%1$s' is not set or empty.", name));
}
return value;
}
/**
* Adds a property to a {@link SubAnt} task.
* @param task the task.
* @param name the name of the property.
* @param value the value of the property.
*/
private void addProp(SubAnt task, String name, String value) {
Property prop = new Property();
prop.setName(name);
prop.setValue(value);
task.addProperty(prop);
}
}