blob: 7f8bcfb352ec3c13a25020a5cad5828d9c84948b [file] [log] [blame]
/*
* Copyright (C) 2014 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.build.gradle.tasks;
import com.android.annotations.NonNull;
import com.android.build.FilterData;
import com.android.build.OutputFile;
import com.android.build.gradle.api.ApkOutputFile;
import com.android.build.gradle.internal.model.FilterDataImpl;
import com.android.builder.model.SigningConfig;
import com.android.builder.packaging.SigningException;
import com.android.builder.signing.SignedJarBuilder;
import com.android.ide.common.signing.KeytoolException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Callables;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFiles;
import org.gradle.api.tasks.ParallelizableTask;
import org.gradle.api.tasks.TaskAction;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Package each split resources into a specific signed apk file.
*/
@ParallelizableTask
public class PackageSplitRes extends SplitRelatedTask {
private Set<String> densitySplits;
private Set<String> languageSplits;
private String outputBaseName;
private SigningConfig signingConfig;
/**
* This directories are not officially input/output to the task as they are shared among tasks.
* To be parallelizable, we must only define our I/O in terms of files...
*/
private File inputDirectory;
private File outputDirectory;
@InputFiles
public List<File> getInputFiles() {
final ImmutableList.Builder<File> builder = ImmutableList.builder();
forEachInputFile(new SplitFileHandler() {
@Override
public void execute(String split, File file) {
builder.add(file);
}
});
return builder.build();
}
@OutputFiles
public List<File> getOutputFiles() {
ImmutableList.Builder<File> builder = ImmutableList.builder();
for (ApkOutputFile apk : getOutputSplitFiles()) {
builder.add(apk.getOutputFile());
}
return builder.build();
}
@Override
public File getApkMetadataFile() {
return null;
}
/**
* Calculates the list of output files, coming from the list of input files, mangling the output
* file name.
*/
@Override
public List<ApkOutputFile> getOutputSplitFiles() {
final ImmutableList.Builder<ApkOutputFile> builder = ImmutableList.builder();
forEachInputFile(new SplitFileHandler() {
@Override
public void execute(String split, File file) {
// find the split identification, if null, the split is not requested any longer.
FilterData filterData = null;
for (String density : densitySplits) {
if (split.startsWith(density)) {
filterData = FilterDataImpl.build(
OutputFile.FilterType.DENSITY.toString(), density);
}
}
if (languageSplits.contains(unMangleSplitName(split))) {
filterData = FilterDataImpl.build(
OutputFile.FilterType.LANGUAGE.toString(), unMangleSplitName(split));
}
if (filterData != null) {
builder.add(new ApkOutputFile(
OutputFile.OutputType.SPLIT,
ImmutableList.of(filterData),
Callables.returning(
new File(outputDirectory, getOutputFileNameForSplit(split)))));
}
}
});
return builder.build();
}
@TaskAction
protected void doFullTaskAction() {
forEachInputFile(
new SplitFileHandler() {
@Override
public void execute(String split, File file) {
File outFile = new File(outputDirectory,
getOutputFileNameForSplit(split));
try {
getBuilder().signApk(file, signingConfig, outFile);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (KeytoolException e) {
throw new RuntimeException(e);
} catch (SigningException e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (SignedJarBuilder.IZipEntryFilter.ZipAbortException e) {
throw new RuntimeException(e);
} catch (com.android.builder.signing.SigningException e) {
throw new RuntimeException(e);
}
}
});
}
private interface SplitFileHandler {
void execute(String split, File file);
}
/**
* Runs the handler for each task input file, providing the split identifier (possibly with a
* suffix generated by aapt) and the input file handle.
*/
private void forEachInputFile(SplitFileHandler handler) {
Pattern resourcePattern = Pattern.compile("resources-" + outputBaseName + ".ap__(.*)");
// make a copy of the expected densities and languages filters.
List<String> densitiesCopy = Lists.newArrayList(densitySplits);
List<String> languagesCopy = Lists.newArrayList(languageSplits);
// resources- and .ap_ should be shared in a setting somewhere. see BasePlugin:1206
File[] fileLists = inputDirectory.listFiles();
if (fileLists != null) {
for (File file : fileLists) {
Matcher match = resourcePattern.matcher(file.getName());
// each time we match, we remove the associated filter from our copies.
if (match.matches() && !match.group(1).isEmpty()
&& isValidSplit(densitiesCopy, languagesCopy, match.group(1))) {
handler.execute(match.group(1), file);
}
}
}
// manually invoke the handler for filters we did not find associated files, apply best
// guess on the actual file names.
for (String density : densitiesCopy) {
handler.execute(density,
new File(inputDirectory, "resources-" + outputBaseName + ".ap__" + density));
}
for (String language : languagesCopy) {
handler.execute(language,
new File(inputDirectory, "resources-" + outputBaseName + ".ap__" + language));
}
}
/**
* Returns true if the passed split identifier is a valid identifier (valid mean it is a
* requested split for this task). A density split identifier can be suffixed with characters
* added by aapt.
*/
private static boolean isValidSplit(
List<String> densities,
List<String> languages,
@NonNull String splitWithOptionalSuffix) {
for (String density : densities) {
if (splitWithOptionalSuffix.startsWith(density)) {
densities.remove(density);
return true;
}
}
String mangledName = unMangleSplitName(splitWithOptionalSuffix);
if (languages.contains(mangledName)) {
languages.remove(mangledName);
return true;
}
return false;
}
public String getOutputFileNameForSplit(final String split) {
String archivesBaseName = (String)getProject().getProperties().get("archivesBaseName");
String apkName = archivesBaseName + "-" + outputBaseName + "_" + split;
return apkName + (signingConfig == null ? "-unsigned.apk" : "-unaligned.apk");
}
@Override
public List<FilterData> getSplitsData() {
ImmutableList.Builder<FilterData> filterDataBuilder = ImmutableList.builder();
addAllFilterData(filterDataBuilder, densitySplits, OutputFile.FilterType.DENSITY);
addAllFilterData(filterDataBuilder, languageSplits, OutputFile.FilterType.LANGUAGE);
return filterDataBuilder.build();
}
/**
* Un-mangle a split name as created by the aapt tool to retrieve a split name as configured in
* the project's build.gradle.
*
* when dealing with several split language in a single split, each language (+ optional region)
* will be seperated by an underscore.
*
* note that there is currently an aapt bug, remove the 'r' in the region so for instance,
* fr-rCA becomes fr-CA, temporarily put it back until it is fixed.
*
* @param splitWithOptionalSuffix the mangled split name.
*/
public static String unMangleSplitName(String splitWithOptionalSuffix) {
String mangledName = splitWithOptionalSuffix.replaceAll("_", ",");
return mangledName.contains("-r") ? mangledName : mangledName.replace("-", "-r");
}
@Input
public Set<String> getDensitySplits() {
return densitySplits;
}
public void setDensitySplits(Set<String> densitySplits) {
this.densitySplits = densitySplits;
}
@Input
public Set<String> getLanguageSplits() {
return languageSplits;
}
public void setLanguageSplits(Set<String> languageSplits) {
this.languageSplits = languageSplits;
}
@Input
public String getOutputBaseName() {
return outputBaseName;
}
public void setOutputBaseName(String outputBaseName) {
this.outputBaseName = outputBaseName;
}
@Nested
@Optional
public SigningConfig getSigningConfig() {
return signingConfig;
}
public void setSigningConfig(SigningConfig signingConfig) {
this.signingConfig = signingConfig;
}
public File getInputDirectory() {
return inputDirectory;
}
public void setInputDirectory(File inputDirectory) {
this.inputDirectory = inputDirectory;
}
public File getOutputDirectory() {
return outputDirectory;
}
public void setOutputDirectory(File outputDirectory) {
this.outputDirectory = outputDirectory;
}
}