/* * 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; } }