blob: 0e3f5612ffd298bcd94961feca58710720c2a216 [file] [log] [blame]
/*
* Copyright (C) 2017 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 static com.android.SdkConstants.DOT_ZIP;
import com.android.annotations.NonNull;
import com.android.build.gradle.internal.publishing.AndroidArtifacts;
import com.android.build.gradle.internal.scope.BuildArtifactsHolder;
import com.android.build.gradle.internal.scope.BuildOutput;
import com.android.build.gradle.internal.scope.ExistingBuildElements;
import com.android.build.gradle.internal.scope.InstantAppOutputScope;
import com.android.build.gradle.internal.scope.InternalArtifactType;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.tasks.ModuleMetadata;
import com.android.build.gradle.internal.tasks.NonIncrementalTask;
import com.android.build.gradle.internal.tasks.Workers;
import com.android.build.gradle.internal.tasks.factory.TaskCreationAction;
import com.android.ide.common.workers.ExecutorServiceAdapter;
import com.android.ide.common.workers.WorkerExecutorFacade;
import com.android.tools.build.apkzlib.zip.ZFile;
import com.android.tools.build.apkzlib.zip.ZFileOptions;
import com.android.tools.build.apkzlib.zip.compress.DeflateExecutionCompressor;
import com.android.utils.FileUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.Deflater;
import javax.inject.Inject;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskProvider;
/** Task to bundle a bundle of feature APKs. */
public abstract class BundleInstantApp extends NonIncrementalTask {
@Override
protected void doTaskAction() throws IOException {
// FIXME: Make this task incremental.
try (WorkerExecutorFacade workers = getWorkerFacadeWithWorkers()) {
workers.submit(
BundleInstantAppRunnable.class,
new BundleInstantAppParams(
getProjectName(),
getPath(),
getBundleDirectory().get().getAsFile(),
bundleName,
ModuleMetadata.load(applicationMetadataFile.getSingleFile())
.getApplicationId(),
new TreeSet<>(apkDirectories.getFiles())));
}
}
@OutputDirectory
@NonNull
public abstract DirectoryProperty getBundleDirectory();
@Input
@NonNull
public String getBundleName() {
return bundleName;
}
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
@NonNull
public FileCollection getApplicationMetadataFile() {
return applicationMetadataFile;
}
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
@NonNull
public FileCollection getApkDirectories() {
return apkDirectories;
}
private String bundleName;
private FileCollection applicationMetadataFile;
private FileCollection apkDirectories;
public static class CreationAction extends TaskCreationAction<BundleInstantApp> {
public CreationAction(@NonNull VariantScope scope, @NonNull File bundleDirectory) {
this.scope = scope;
this.bundleDirectory = bundleDirectory;
}
@NonNull
@Override
public String getName() {
return scope.getTaskName("package", "InstantAppBundle");
}
@NonNull
@Override
public Class<BundleInstantApp> getType() {
return BundleInstantApp.class;
}
@Override
public void handleProvider(@NonNull TaskProvider<? extends BundleInstantApp> taskProvider) {
super.handleProvider(taskProvider);
scope.getArtifacts()
.producesDir(
InternalArtifactType.INSTANTAPP_BUNDLE,
BuildArtifactsHolder.OperationType.INITIAL,
taskProvider,
BundleInstantApp::getBundleDirectory,
bundleDirectory.getAbsolutePath(),
"");
}
@Override
public void configure(@NonNull BundleInstantApp bundleInstantApp) {
bundleInstantApp.setVariantName(scope.getFullVariantName());
bundleInstantApp.bundleName =
scope.getGlobalScope().getProjectBaseName()
+ "-"
+ scope.getVariantConfiguration().getBaseName()
+ DOT_ZIP;
bundleInstantApp.applicationMetadataFile =
scope.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH,
AndroidArtifacts.ArtifactScope.PROJECT,
AndroidArtifacts.ArtifactType.FEATURE_APPLICATION_ID_DECLARATION);
bundleInstantApp.apkDirectories =
scope.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
AndroidArtifacts.ArtifactScope.PROJECT,
AndroidArtifacts.ArtifactType.APK);
}
private final VariantScope scope;
private final File bundleDirectory;
}
private static class BundleInstantAppRunnable implements Runnable {
private final BundleInstantAppParams params;
@Inject
public BundleInstantAppRunnable(BundleInstantAppParams params) {
this.params = params;
}
@Override
public void run() {
try {
FileUtils.mkdirs(params.bundleDirectory);
File bundleFile = new File(params.bundleDirectory, params.bundleName);
FileUtils.deleteIfExists(bundleFile);
ZFileOptions zFileOptions = new ZFileOptions();
try (ExecutorServiceAdapter executor =
Workers.INSTANCE.withThreads(params.projectName, params.taskOwnerName)) {
zFileOptions.setCompressor(
new DeflateExecutionCompressor(
(compressJob) ->
executor.submit(
CompressorRunnable.class,
new CompressorParams(compressJob)),
Deflater.DEFAULT_COMPRESSION));
try (ZFile file = ZFile.openReadWrite(bundleFile, zFileOptions)) {
for (File apkDirectory : params.apkDirectories) {
for (BuildOutput buildOutput :
ExistingBuildElements.from(
InternalArtifactType.APK, apkDirectory)) {
File apkFile = buildOutput.getOutputFile();
try (FileInputStream fileInputStream =
new FileInputStream(apkFile)) {
file.add(apkFile.getName(), fileInputStream);
}
}
}
}
}
// Write the json output.
InstantAppOutputScope instantAppOutputScope =
new InstantAppOutputScope(
params.applicationId,
bundleFile,
new ArrayList<>(params.apkDirectories));
instantAppOutputScope.save(params.bundleDirectory);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static class CompressorRunnable implements Runnable {
private final Runnable compressJob;
@Inject
CompressorRunnable(CompressorParams params) {
this.compressJob = params.compressJob;
}
@Override
public void run() {
compressJob.run();
}
}
static class CompressorParams implements Serializable {
private final Runnable compressJob;
CompressorParams(Runnable compressJob) {
this.compressJob = compressJob;
}
}
}
private static class BundleInstantAppParams implements Serializable {
@NonNull private final String projectName;
@NonNull private final String taskOwnerName;
@NonNull private final File bundleDirectory;
@NonNull private final String bundleName;
@NonNull private final String applicationId;
@NonNull private final Set<File> apkDirectories;
BundleInstantAppParams(
@NonNull String projectName,
@NonNull String taskOwnerName,
@NonNull File bundleDirectory,
@NonNull String bundleName,
@NonNull String applicationId,
@NonNull Set<File> apkDirectories) {
this.projectName = projectName;
this.taskOwnerName = taskOwnerName;
this.bundleDirectory = bundleDirectory;
this.bundleName = bundleName;
this.applicationId = applicationId;
this.apkDirectories = apkDirectories;
}
}
}