blob: d0d5247a20206c35288e635ccdaa4022bac80d6c [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.internal.tasks;
import static com.android.SdkConstants.DOT_ANDROID_PACKAGE;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FD_RES_RAW;
import static com.android.SdkConstants.FD_RES_XML;
import static com.android.build.gradle.internal.scope.InternalArtifactType.APK;
import static com.android.builder.core.BuilderConstants.ANDROID_WEAR;
import static com.android.builder.core.BuilderConstants.ANDROID_WEAR_MICRO_APK;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.process.GradleProcessExecutor;
import com.android.build.gradle.internal.res.Aapt2MavenUtils;
import com.android.build.gradle.internal.scope.BuildElements;
import com.android.build.gradle.internal.scope.BuildOutput;
import com.android.build.gradle.internal.scope.ExistingBuildElements;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction;
import com.android.build.gradle.internal.variant.ApkVariantData;
import com.android.builder.core.ApkInfoParser;
import com.android.ide.common.process.ProcessException;
import com.android.utils.FileUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskProvider;
/** Task to generate micro app data res file. */
public class GenerateApkDataTask extends NonIncrementalTask {
@Nullable private FileCollection apkDirectoryFileCollection;
private File resOutputDir;
private File manifestFile;
private Supplier<String> mainPkgName;
private int minSdkVersion;
private int targetSdkVersion;
private FileCollection aapt2Executable;
@Override
protected void doTaskAction() throws IOException, ProcessException {
// if the FileCollection contains no file, then there's nothing to do just abort.
File apkDirectory = null;
if (apkDirectoryFileCollection != null) {
Set<File> files = apkDirectoryFileCollection.getFiles();
if (files.isEmpty()) {
return;
}
if (files.size() > 1) {
throw new IllegalStateException(
"Wear App dependency resolve to more than one file: " + files);
}
apkDirectory = Iterables.getOnlyElement(files);
if (!apkDirectory.isDirectory()) {
throw new IllegalStateException(
"Wear App dependency does not resolve to a directory: " + files);
}
}
// always empty output dir.
File outDir = getResOutputDir();
FileUtils.cleanOutputDir(outDir);
if (apkDirectory != null) {
BuildElements apks = ExistingBuildElements.from(APK, apkDirectory);
if (apks.isEmpty()) {
throw new IllegalStateException("Wear App dependency resolve to zero APK");
}
if (apks.size() > 1) {
throw new IllegalStateException(
"Wear App dependency resolve to more than one APK: "
+ apks.stream()
.map(BuildOutput::getOutputFile)
.collect(Collectors.toList()));
}
File apk = Iterables.getOnlyElement(apks).getOutputFile();
// copy the file into the destination, by sanitizing the name first.
File rawDir = new File(outDir, FD_RES_RAW);
FileUtils.mkdirs(rawDir);
File to = new File(rawDir, ANDROID_WEAR_MICRO_APK + DOT_ANDROID_PACKAGE);
Files.copy(apk, to);
generateApkData(apk, outDir, getMainPkgName(), aapt2Executable.getSingleFile());
} else {
generateUnbundledWearApkData(outDir, getMainPkgName());
}
generateApkDataEntryInManifest(minSdkVersion, targetSdkVersion, manifestFile);
}
private void generateApkData(
@NonNull File apkFile,
@NonNull File outResFolder,
@NonNull String mainPkgName,
@NonNull File aapt2Executable)
throws ProcessException, IOException {
ApkInfoParser parser =
new ApkInfoParser(aapt2Executable, new GradleProcessExecutor(getProject()));
ApkInfoParser.ApkInfo apkInfo = parser.parseApk(apkFile);
if (!apkInfo.getPackageName().equals(mainPkgName)) {
throw new RuntimeException(
"The main and the micro apps do not have the same package name.");
}
String content =
String.format(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<wearableApp package=\"%1$s\">\n"
+ " <versionCode>%2$s</versionCode>\n"
+ " <versionName>%3$s</versionName>\n"
+ " <rawPathResId>%4$s</rawPathResId>\n"
+ "</wearableApp>",
apkInfo.getPackageName(),
apkInfo.getVersionCode(),
apkInfo.getVersionName(),
ANDROID_WEAR_MICRO_APK);
// xml folder
File resXmlFile = new File(outResFolder, FD_RES_XML);
FileUtils.mkdirs(resXmlFile);
Files.asCharSink(new File(resXmlFile, ANDROID_WEAR_MICRO_APK + DOT_XML), Charsets.UTF_8)
.write(content);
}
private static void generateUnbundledWearApkData(
@NonNull File outResFolder, @NonNull String mainPkgName) throws IOException {
String content =
String.format(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<wearableApp package=\"%1$s\">\n"
+ " <unbundled />\n"
+ "</wearableApp>",
mainPkgName);
// xml folder
File resXmlFile = new File(outResFolder, FD_RES_XML);
FileUtils.mkdirs(resXmlFile);
Files.asCharSink(new File(resXmlFile, ANDROID_WEAR_MICRO_APK + DOT_XML), Charsets.UTF_8)
.write(content);
}
private static void generateApkDataEntryInManifest(
int minSdkVersion, int targetSdkVersion, @NonNull File manifestFile)
throws IOException {
StringBuilder content = new StringBuilder();
content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
.append("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n")
.append(" package=\"${packageName}\">\n")
.append(" <uses-sdk android:minSdkVersion=\"")
.append(minSdkVersion)
.append("\"");
if (targetSdkVersion != -1) {
content.append(" android:targetSdkVersion=\"").append(targetSdkVersion).append("\"");
}
content.append("/>\n");
content.append(" <application>\n")
.append(" <meta-data android:name=\"" + ANDROID_WEAR + "\"\n")
.append(" android:resource=\"@xml/" + ANDROID_WEAR_MICRO_APK)
.append("\" />\n")
.append(" </application>\n")
.append("</manifest>\n");
Files.asCharSink(manifestFile, Charsets.UTF_8).write(content);
}
@OutputDirectory
public File getResOutputDir() {
return resOutputDir;
}
public void setResOutputDir(File resOutputDir) {
this.resOutputDir = resOutputDir;
}
@InputFiles
@Optional
public FileCollection getApkFileCollection() {
return apkDirectoryFileCollection;
}
@Input
public String getMainPkgName() {
return mainPkgName.get();
}
@Input
public int getMinSdkVersion() {
return minSdkVersion;
}
public void setMinSdkVersion(int minSdkVersion) {
this.minSdkVersion = minSdkVersion;
}
@Input
public int getTargetSdkVersion() {
return targetSdkVersion;
}
@InputFiles
@PathSensitive(PathSensitivity.NONE)
public FileCollection getAapt2Executable() {
return aapt2Executable;
}
public void setTargetSdkVersion(int targetSdkVersion) {
this.targetSdkVersion = targetSdkVersion;
}
@OutputFile
public File getManifestFile() {
return manifestFile;
}
public static class CreationAction extends VariantTaskCreationAction<GenerateApkDataTask> {
@Nullable private FileCollection apkFileCollection;
public CreationAction(
@NonNull VariantScope scope, @Nullable FileCollection apkFileCollection) {
super(scope);
this.apkFileCollection = apkFileCollection;
}
@Override
@NonNull
public String getName() {
return getVariantScope().getTaskName("handle", "MicroApk");
}
@Override
@NonNull
public Class<GenerateApkDataTask> getType() {
return GenerateApkDataTask.class;
}
@Override
public void handleProvider(
@NonNull TaskProvider<? extends GenerateApkDataTask> taskProvider) {
super.handleProvider(taskProvider);
getVariantScope().getTaskContainer().setMicroApkTask(taskProvider);
getVariantScope().getTaskContainer().setGenerateApkDataTask(taskProvider);
}
@Override
public void configure(@NonNull GenerateApkDataTask task) {
super.configure(task);
VariantScope scope = getVariantScope();
final ApkVariantData variantData = (ApkVariantData) scope.getVariantData();
final GradleVariantConfiguration variantConfiguration =
variantData.getVariantConfiguration();
task.setResOutputDir(scope.getMicroApkResDirectory());
task.apkDirectoryFileCollection = apkFileCollection;
task.manifestFile = scope.getMicroApkManifestFile();
task.mainPkgName = variantConfiguration::getApplicationId;
task.minSdkVersion = variantConfiguration.getMinSdkVersion().getApiLevel();
task.targetSdkVersion = variantConfiguration.getTargetSdkVersion().getApiLevel();
task.aapt2Executable = Aapt2MavenUtils.getAapt2FromMaven(scope.getGlobalScope());
}
}
}