package com.android.build.gradle.tasks;
import static com.android.builder.model.AndroidProject.FD_INTERMEDIATES;
import com.android.annotations.NonNull;
import com.android.build.gradle.internal.annotations.ApkFile;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.dsl.AbiSplitOptions;
import com.android.build.gradle.internal.dsl.CoreSigningConfig;
import com.android.build.gradle.internal.dsl.PackagingOptions;
import com.android.build.gradle.internal.scope.ConventionMappingHelper;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.VariantOutputScope;
import com.android.build.gradle.internal.tasks.FileSupplier;
import com.android.build.gradle.internal.tasks.IncrementalTask;
import com.android.build.gradle.internal.tasks.ValidateSigningTask;
import com.android.build.gradle.internal.variant.ApkVariantData;
import com.android.build.gradle.internal.variant.ApkVariantOutputData;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.builder.packaging.DuplicateFileException;
import com.android.builder.signing.SignedJarBuilder;
import com.android.utils.StringHelper;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import org.codehaus.groovy.runtime.StringGroovyMethods;
import org.gradle.api.Task;
import org.gradle.api.file.FileTree;
import org.gradle.api.logging.Logger;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.ParallelizableTask;
import org.gradle.tooling.BuildException;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.Callable;
@ParallelizableTask
public class PackageApplication extends IncrementalTask implements FileSupplier {
// ----- PUBLIC TASK API -----
@InputFile
public File getResourceFile() {
return resourceFile;
}
public void setResourceFile(File resourceFile) {
this.resourceFile = resourceFile;
}
@InputDirectory
public File getDexFolder() {
return dexFolder;
}
public void setDexFolder(File dexFolder) {
this.dexFolder = dexFolder;
}
@InputFiles
public Collection<File> getDexedLibraries() {
return dexedLibraries;
}
public void setDexedLibraries(Collection<File> dexedLibraries) {
this.dexedLibraries = dexedLibraries;
}
@InputDirectory
@Optional
public File getJavaResourceDir() {
return javaResourceDir;
}
public void setJavaResourceDir(File javaResourceDir) {
this.javaResourceDir = javaResourceDir;
}
public Set<File> getJniFolders() {
return jniFolders;
}
public void setJniFolders(Set<File> jniFolders) {
this.jniFolders = jniFolders;
}
public File getMergingFolder() {
return mergingFolder;
}
public void setMergingFolder(File mergingFolder) {
this.mergingFolder = mergingFolder;
}
@OutputFile
public File getOutputFile() {
return outputFile;
}
public void setOutputFile(File outputFile) {
this.outputFile = outputFile;
}
@Input
@Optional
public Set<String> getAbiFilters() {
return abiFilters;
}
public void setAbiFilters(Set<String> abiFilters) {
this.abiFilters = abiFilters;
}
// ----- PRIVATE TASK API -----
private File resourceFile;
private File dexFolder;
private Collection<File> dexedLibraries;
private File javaResourceDir;
private Set<File> jniFolders;
private File mergingFolder;
@ApkFile
private File outputFile;
private Set<String> abiFilters;
private Set<File> packagedJars;
private boolean jniDebugBuild;
private CoreSigningConfig signingConfig;
private PackagingOptions packagingOptions;
private SignedJarBuilder.IZipEntryFilter packagingOptionsFilter;
@InputFiles
public Set<File> getPackagedJars() {
return packagedJars;
}
public void setPackagedJars(Set<File> packagedJars) {
this.packagedJars = packagedJars;
}
@Input
public boolean getJniDebugBuild() {
return jniDebugBuild;
}
public boolean isJniDebugBuild() {
return jniDebugBuild;
}
public void setJniDebugBuild(boolean jniDebugBuild) {
this.jniDebugBuild = jniDebugBuild;
}
@Nested
@Optional
public CoreSigningConfig getSigningConfig() {
return signingConfig;
}
public void setSigningConfig(CoreSigningConfig signingConfig) {
this.signingConfig = signingConfig;
}
@Nested
public PackagingOptions getPackagingOptions() {
return packagingOptions;
}
public void setPackagingOptions(PackagingOptions packagingOptions) {
this.packagingOptions = packagingOptions;
}
public SignedJarBuilder.IZipEntryFilter getPackagingOptionsFilter() {
return packagingOptionsFilter;
}
public void setPackagingOptionsFilter(SignedJarBuilder.IZipEntryFilter filter) {
this.packagingOptionsFilter = filter;
}
@InputFiles
public FileTree getNativeLibraries() {
FileTree src = null;
Set<File> folders = getJniFolders();
if (!folders.isEmpty()) {
src = getProject().files(new ArrayList<Object>(folders)).getAsFileTree();
}
return src == null ? getProject().files().getAsFileTree() : src;
}
@Override
protected void doFullTaskAction() {
try {
final File dir = getJavaResourceDir();
getBuilder().packageApk(getResourceFile().getAbsolutePath(), getDexFolder(),
getDexedLibraries(), getPackagedJars(),
(dir == null ? null : dir.getAbsolutePath()), getJniFolders(),
getMergingFolder(), getAbiFilters(), getJniDebugBuild(), getSigningConfig(),
getPackagingOptions(),
getPackagingOptionsFilter(),
getOutputFile().getAbsolutePath());
} catch (DuplicateFileException e) {
Logger logger = getLogger();
logger.error("Error: duplicate files during packaging of APK " + getOutputFile()
.getAbsolutePath());
logger.error("\tPath in archive: " + e.getArchivePath());
logger.error("\tOrigin 1: " + e.getFile1());
logger.error("\tOrigin 2: " + e.getFile2());
logger.error("You can ignore those files in your build.gradle:");
logger.error("\tandroid {");
logger.error("\t packagingOptions {");
logger.error("\t exclude \'" + e.getArchivePath() + "\'");
logger.error("\t }");
logger.error("\t}");
throw new BuildException(e.getMessage(), e);
} catch (Exception e) {
throw new BuildException(e.getMessage(), e);
}
}
// ----- FileSupplierTask -----
@Override
public File get() {
return getOutputFile();
}
@NonNull
@Override
public Task getTask() {
return this;
}
// ----- ConfigAction -----
public static class ConfigAction implements TaskConfigAction<PackageApplication> {
private VariantOutputScope scope;
public ConfigAction(VariantOutputScope scope) {
this.scope = scope;
}
@Override
public String getName() {
return scope.getTaskName("package");
}
@Override
public Class<PackageApplication> getType() {
return PackageApplication.class;
}
@Override
public void execute(PackageApplication packageApp) {
final ApkVariantData variantData = (ApkVariantData) scope.getVariantScope().getVariantData();
final ApkVariantOutputData variantOutputData = (ApkVariantOutputData) scope
.getVariantOutputData();
final GradleVariantConfiguration config = scope.getVariantScope().getVariantConfiguration();
variantOutputData.packageApplicationTask = packageApp;
packageApp.setAndroidBuilder(scope.getGlobalScope().getAndroidBuilder());
packageApp.setVariantName(
scope.getVariantScope().getVariantConfiguration().getFullName());
if (config.isMinifyEnabled() && config.getBuildType().isShrinkResources() && !config
.getUseJack()) {
ConventionMappingHelper.map(packageApp, "resourceFile", new Callable<File>() {
@Override
public File call() {
return scope.getCompressedResourceFile();
}
});
} else {
ConventionMappingHelper.map(packageApp, "resourceFile", new Callable<File>() {
@Override
public File call() {
return variantOutputData.processResourcesTask.getPackageOutputFile();
}
});
}
ConventionMappingHelper.map(packageApp, "dexFolder", new Callable<File>() {
@Override
public File call() {
return scope.getVariantScope().getDexOutputFolder();
}
});
ConventionMappingHelper.map(packageApp, "dexedLibraries",
new Callable<Collection<File>>() {
@Override
public Collection<File> call() {
if (config.isMultiDexEnabled() && !config.isLegacyMultiDexMode()
&& variantData.preDexTask != null) {
return scope.getGlobalScope().getProject()
.fileTree(variantData.preDexTask.getOutputFolder())
.getFiles();
}
return Collections.emptyList();
}
});
scope.getVariantScope().getJavaResourcesProvider();
ConventionMappingHelper.map(packageApp, "packagedJars", new Callable<Set<File>>() {
@Override
public Set<File> call() {
ImmutableSet.Builder<File> jarFiles = ImmutableSet.builder();
for (JavaResourcesProvider.JavaResourcesLocation javaResourcesProvider :
scope.getVariantScope().getJavaResourcesProvider().getJavaResourcesLocations()) {
if (javaResourcesProvider.type == JavaResourcesProvider.Type.JAR) {
jarFiles.add(javaResourcesProvider.location);
}
}
return jarFiles.build();
}
});
ConventionMappingHelper.map(packageApp, "javaResourceDir", new Callable<File>() {
@Override
public File call() {
ImmutableSet.Builder<File> folders = ImmutableSet.builder();
for (JavaResourcesProvider.JavaResourcesLocation javaResourcesProvider :
scope.getVariantScope().getJavaResourcesProvider()
.getJavaResourcesLocations()) {
if (javaResourcesProvider.type == JavaResourcesProvider.Type.FOLDER) {
folders.add(javaResourcesProvider.location);
}
}
ImmutableSet<File> resourceFolders = folders.build();
if (resourceFolders.size() > 1) {
throw new RuntimeException("More than one java resources folders : " +
Joiner.on(",").join(resourceFolders));
}
return resourceFolders.isEmpty() ? null : resourceFolders.iterator().next();
}
});
packageApp.setMergingFolder(new File(scope.getGlobalScope().getIntermediatesDir(),
variantOutputData.getFullName() + "/merging"));
ConventionMappingHelper.map(packageApp, "jniFolders", new Callable<Set<File>>() {
@Override
public Set<File> call() {
if (variantData.getSplitHandlingPolicy() ==
BaseVariantData.SplitHandlingPolicy.PRE_21_POLICY) {
return scope.getVariantScope().getJniFolders();
}
Set<String> filters = AbiSplitOptions.getAbiFilters(
scope.getGlobalScope().getExtension().getSplits().getAbiFilters());
return filters.isEmpty() ? scope.getVariantScope().getJniFolders() : Collections.<File>emptySet();
}
});
ConventionMappingHelper.map(packageApp, "abiFilters", new Callable<Set<String>>() {
@Override
public Set<String> call() throws Exception {
if (variantOutputData.getMainOutputFile().getFilter(com.android.build.OutputFile.ABI) != null) {
return ImmutableSet.of(
variantOutputData.getMainOutputFile()
.getFilter(com.android.build.OutputFile.ABI));
}
return config.getSupportedAbis();
}
});
ConventionMappingHelper.map(packageApp, "jniDebugBuild", new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return config.getBuildType().isJniDebuggable();
}
});
CoreSigningConfig sc = (CoreSigningConfig) config.getSigningConfig();
packageApp.setSigningConfig(sc);
if (sc != null) {
String validateSigningTaskName = "validate" + StringHelper.capitalize(sc.getName()) + "Signing";
ValidateSigningTask validateSigningTask =
(ValidateSigningTask) scope.getGlobalScope().getProject().getTasks().findByName(validateSigningTaskName);
if (validateSigningTask == null) {
validateSigningTask =
scope.getGlobalScope().getProject().getTasks().create(
"validate" + StringHelper.capitalize(sc.getName()) + "Signing",
ValidateSigningTask.class);
validateSigningTask.setAndroidBuilder(scope.getGlobalScope().getAndroidBuilder());
validateSigningTask.setVariantName(
scope.getVariantScope().getVariantConfiguration().getFullName());
validateSigningTask.setSigningConfig(sc);
}
packageApp.dependsOn(validateSigningTask);
}
ConventionMappingHelper.map(packageApp, "packagingOptions", new Callable<PackagingOptions>() {
@Override
public PackagingOptions call() throws Exception {
return scope.getGlobalScope().getExtension().getPackagingOptions();
}
});
ConventionMappingHelper.map(packageApp, "packagingOptionsFilter",
new Callable<SignedJarBuilder.IZipEntryFilter>() {
@Override
public SignedJarBuilder.IZipEntryFilter call() throws Exception {
return scope.getVariantScope().getPackagingOptionsFilter();
}
});
ConventionMappingHelper.map(packageApp, "outputFile", new Callable<File>() {
@Override
public File call() throws Exception {
return scope.getPackageApk();
}
});
}
private ShrinkResources createShrinkResourcesTask(
final ApkVariantOutputData variantOutputData) {
BaseVariantData<?> variantData = (BaseVariantData<?>) variantOutputData.variantData;
ShrinkResources task = scope.getGlobalScope().getProject().getTasks()
.create("shrink" + StringGroovyMethods
.capitalize(variantOutputData.getFullName())
+ "Resources", ShrinkResources.class);
task.setAndroidBuilder(scope.getGlobalScope().getAndroidBuilder());
task.setVariantName(scope.getVariantScope().getVariantConfiguration().getFullName());
task.variantOutputData = variantOutputData;
final String outputBaseName = variantOutputData.getBaseName();
task.setCompressedResources(new File(
scope.getGlobalScope().getBuildDir() + "/" + FD_INTERMEDIATES + "/res/" +
"resources-" + outputBaseName + "-stripped.ap_"));
ConventionMappingHelper.map(task, "uncompressedResources", new Callable<File>() {
@Override
public File call() {
return variantOutputData.processResourcesTask.getPackageOutputFile();
}
});
task.dependsOn(
scope.getVariantScope().getObfuscationTask().getName(),
scope.getManifestProcessorTask().getName(),
variantOutputData.processResourcesTask);
return task;
}
private static File getOptionalDir(File dir) {
if (dir.isDirectory()) {
return dir;
}
return null;
}
}
}