| /* |
| * 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 static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.PROJECT; |
| import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.FEATURE_APPLICATION_ID_DECLARATION; |
| import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.METADATA_BASE_MODULE_DECLARATION; |
| import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH; |
| import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.METADATA_VALUES; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.build.VariantOutput; |
| import com.android.build.gradle.internal.LoggerWrapper; |
| import com.android.build.gradle.internal.core.VariantConfiguration; |
| import com.android.build.gradle.internal.dsl.DslAdaptersKt; |
| import com.android.build.gradle.internal.res.Aapt2MavenUtils; |
| import com.android.build.gradle.internal.res.Aapt2ProcessResourcesRunnable; |
| import com.android.build.gradle.internal.res.LinkingTaskInputAaptOptions; |
| import com.android.build.gradle.internal.res.namespaced.Aapt2DaemonManagerService; |
| import com.android.build.gradle.internal.res.namespaced.Aapt2ServiceKey; |
| import com.android.build.gradle.internal.scope.ApkData; |
| import com.android.build.gradle.internal.scope.BuildArtifactsHolder; |
| import com.android.build.gradle.internal.scope.BuildElements; |
| import com.android.build.gradle.internal.scope.BuildOutput; |
| 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.factory.VariantTaskCreationAction; |
| import com.android.build.gradle.internal.tasks.featuresplit.FeatureSetMetadata; |
| import com.android.build.gradle.options.SyncOptions; |
| import com.android.builder.core.VariantType; |
| import com.android.builder.internal.aapt.AaptOptions; |
| import com.android.builder.internal.aapt.AaptPackageConfig; |
| import com.android.ide.common.workers.WorkerExecutorFacade; |
| import com.android.utils.FileUtils; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.CharMatcher; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSortedMap; |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.util.Comparator; |
| import java.util.Set; |
| import java.util.function.IntSupplier; |
| import java.util.function.Supplier; |
| import kotlin.Pair; |
| import org.gradle.api.file.ConfigurableFileCollection; |
| import org.gradle.api.file.DirectoryProperty; |
| import org.gradle.api.file.FileCollection; |
| import org.gradle.api.file.RegularFile; |
| import org.gradle.api.provider.Provider; |
| import org.gradle.api.tasks.Input; |
| import org.gradle.api.tasks.InputFile; |
| import org.gradle.api.tasks.InputFiles; |
| import org.gradle.api.tasks.Internal; |
| import org.gradle.api.tasks.Nested; |
| import org.gradle.api.tasks.Optional; |
| import org.gradle.api.tasks.OutputDirectory; |
| import org.gradle.api.tasks.PathSensitive; |
| import org.gradle.api.tasks.PathSensitivity; |
| import org.gradle.api.tasks.TaskProvider; |
| |
| /** Generates all metadata (like AndroidManifest.xml) necessary for a ABI dimension split APK. */ |
| public abstract class GenerateSplitAbiRes extends NonIncrementalTask { |
| |
| private Supplier<String> applicationId; |
| private String outputBaseName; |
| |
| // these are the default values set in the variant's configuration, although they |
| // are not directly use in this task, they will be used when versionName and versionCode |
| // is not changed by the user's scripts. Therefore, if those values change, this task |
| // should be considered out of date. |
| private Supplier<String> versionName; |
| private IntSupplier versionCode; |
| |
| // We use a sorted map so the key set order is consistent since it's considered an input. |
| private ImmutableSortedMap<String, ApkData> splits; |
| private boolean debuggable; |
| private AaptOptions aaptOptions; |
| private VariantType variantType; |
| @VisibleForTesting @Nullable Supplier<String> featureNameSupplier; |
| @Nullable private FileCollection applicationIdOverride; |
| private String aapt2Version; |
| |
| @Internal |
| public abstract ConfigurableFileCollection getAapt2FromMaven(); |
| |
| private File mergeBlameFolder; |
| |
| // Not an input as it is only used to rewrite exception and doesn't affect task output |
| private Provider<RegularFile> manifestMergeBlameFile; |
| |
| private Provider<File> androidJarProvider; |
| |
| private SyncOptions.ErrorFormatMode errorFormatMode; |
| |
| @Input |
| public String getApplicationId() { |
| return applicationId.get(); |
| } |
| |
| @Input |
| public int getVersionCode() { |
| return versionCode.getAsInt(); |
| } |
| |
| @Input |
| @Optional |
| public String getVersionName() { |
| return versionName.get(); |
| } |
| |
| @Input |
| public String getOutputBaseName() { |
| return outputBaseName; |
| } |
| |
| @Input |
| public Set<String> getSplits() { |
| return splits.keySet(); |
| } |
| |
| @OutputDirectory |
| public abstract DirectoryProperty getOutputDirectory(); |
| |
| @Input |
| public boolean isDebuggable() { |
| return debuggable; |
| } |
| |
| @Nested |
| public LinkingTaskInputAaptOptions getAaptOptionsInput() { |
| return new LinkingTaskInputAaptOptions(aaptOptions); |
| } |
| |
| @Input |
| @Optional |
| @Nullable |
| public String getFeatureName() { |
| return featureNameSupplier != null ? featureNameSupplier.get() : null; |
| } |
| |
| @InputFiles |
| @Optional |
| @Nullable |
| public FileCollection getApplicationIdOverride() { |
| return applicationIdOverride; |
| } |
| |
| @Input |
| public String getAapt2Version() { |
| return aapt2Version; |
| } |
| |
| @Override |
| protected void doTaskAction() throws IOException { |
| |
| ImmutableList.Builder<BuildOutput> buildOutputs = ImmutableList.builder(); |
| |
| try (WorkerExecutorFacade workerExecutor = getWorkerFacadeWithWorkers()) { |
| for (String split : splits.keySet()) { |
| File resPackageFile = getOutputFileForSplit(split); |
| File manifestFile = generateSplitManifest(split, splits.get(split)); |
| |
| AaptPackageConfig aaptConfig = |
| new AaptPackageConfig.Builder() |
| .setManifestFile(manifestFile) |
| .setOptions(aaptOptions) |
| .setDebuggable(debuggable) |
| .setResourceOutputApk(resPackageFile) |
| .setVariantType(variantType) |
| .setAndroidTarget(androidJarProvider.get()) |
| .build(); |
| |
| Aapt2ServiceKey aapt2ServiceKey = |
| Aapt2DaemonManagerService.registerAaptService( |
| getAapt2FromMaven(), new LoggerWrapper(getLogger())); |
| Aapt2ProcessResourcesRunnable.Params params = |
| new Aapt2ProcessResourcesRunnable.Params( |
| aapt2ServiceKey, |
| aaptConfig, |
| errorFormatMode, |
| mergeBlameFolder, |
| getManifestMergeBlameFile()); |
| workerExecutor.submit(Aapt2ProcessResourcesRunnable.class, params); |
| |
| buildOutputs.add( |
| new BuildOutput( |
| InternalArtifactType.ABI_PROCESSED_SPLIT_RES, |
| splits.get(split), |
| resPackageFile)); |
| } |
| } |
| new BuildElements(buildOutputs.build()).save(getOutputDirectory().get().getAsFile()); |
| } |
| |
| @Nullable |
| private File getManifestMergeBlameFile() { |
| if (manifestMergeBlameFile.isPresent()) { |
| return manifestMergeBlameFile.get().getAsFile(); |
| } |
| return null; |
| } |
| |
| @InputFile |
| @PathSensitive(PathSensitivity.NONE) |
| public Provider<File> getAndroidJar() { |
| return androidJarProvider; |
| } |
| |
| @VisibleForTesting |
| File generateSplitManifest(String split, ApkData apkInfo) throws IOException { |
| // Split name can only contains 0-9, a-z, A-Z, '.' and '_'. Replace all other |
| // characters with underscore. |
| CharMatcher charMatcher = |
| CharMatcher.inRange('0', '9') |
| .or(CharMatcher.inRange('A', 'Z')) |
| .or(CharMatcher.inRange('a', 'z')) |
| .or(CharMatcher.is('_')) |
| .or(CharMatcher.is('.')) |
| .negate(); |
| |
| String featureName = getFeatureName(); |
| |
| String encodedSplitName = |
| (featureName != null ? featureName + "." : "") |
| + "config." |
| + charMatcher.replaceFrom(split, '_'); |
| |
| File tmpDirectory = new File(getOutputDirectory().get().getAsFile(), split); |
| FileUtils.mkdirs(tmpDirectory); |
| |
| File tmpFile = new File(tmpDirectory, "AndroidManifest.xml"); |
| |
| String versionNameToUse = apkInfo.getVersionName(); |
| if (versionNameToUse == null) { |
| versionNameToUse = String.valueOf(apkInfo.getVersionCode()); |
| } |
| |
| // Override the applicationId for features. |
| String manifestAppId; |
| if (applicationIdOverride != null && !applicationIdOverride.isEmpty()) { |
| manifestAppId = |
| ModuleMetadata.load(applicationIdOverride.getSingleFile()).getApplicationId(); |
| } else { |
| manifestAppId = applicationId.get(); |
| } |
| |
| try (OutputStreamWriter fileWriter = |
| new OutputStreamWriter( |
| new BufferedOutputStream(new FileOutputStream(tmpFile)), "UTF-8")) { |
| |
| fileWriter.append( |
| "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" |
| + "<manifest" |
| + " xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" |
| + " package=\"" |
| + manifestAppId |
| + "\"\n" |
| + " android:versionCode=\"" |
| + apkInfo.getVersionCode() |
| + "\"\n" |
| + " android:versionName=\"" |
| + versionNameToUse |
| + "\"\n"); |
| |
| if (featureName != null) { |
| fileWriter.append(" configForSplit=\"" + featureName + "\"\n"); |
| } |
| |
| fileWriter.append( |
| " split=\"" |
| + encodedSplitName |
| + "\"\n" |
| + " targetABI=\"" |
| + split |
| + "\">\n" |
| + " <uses-sdk android:minSdkVersion=\"21\"/>\n" |
| + "</manifest> "); |
| fileWriter.flush(); |
| } |
| return tmpFile; |
| } |
| |
| // FIX ME : this calculation should move to SplitScope.Split interface |
| private File getOutputFileForSplit(final String split) { |
| return new File( |
| getOutputDirectory().get().getAsFile(), |
| "resources-" + getOutputBaseName() + "-" + split + ".ap_"); |
| } |
| |
| // ----- CreationAction ----- |
| |
| public static class CreationAction extends VariantTaskCreationAction<GenerateSplitAbiRes> { |
| |
| @NonNull private final FeatureSetMetadata.SupplierProvider provider; |
| |
| public CreationAction(@NonNull VariantScope scope) { |
| this(scope, FeatureSetMetadata.getInstance()); |
| } |
| |
| @VisibleForTesting |
| CreationAction( |
| @NonNull VariantScope scope, |
| @NonNull FeatureSetMetadata.SupplierProvider provider) { |
| super(scope); |
| this.provider = provider; |
| } |
| |
| @Override |
| @NonNull |
| public String getName() { |
| return getVariantScope().getTaskName("generate", "SplitAbiRes"); |
| } |
| |
| @Override |
| @NonNull |
| public Class<GenerateSplitAbiRes> getType() { |
| return GenerateSplitAbiRes.class; |
| } |
| |
| @Override |
| public void handleProvider( |
| @NonNull TaskProvider<? extends GenerateSplitAbiRes> taskProvider) { |
| super.handleProvider(taskProvider); |
| |
| getVariantScope() |
| .getArtifacts() |
| .producesDir( |
| InternalArtifactType.ABI_PROCESSED_SPLIT_RES, |
| BuildArtifactsHolder.OperationType.INITIAL, |
| taskProvider, |
| GenerateSplitAbiRes::getOutputDirectory, |
| "out"); |
| } |
| |
| @Override |
| public void configure(@NonNull GenerateSplitAbiRes task) { |
| super.configure(task); |
| VariantScope scope = getVariantScope(); |
| |
| final VariantConfiguration config = scope.getVariantConfiguration(); |
| VariantType variantType = config.getType(); |
| |
| if (variantType.isFeatureSplit()) { |
| task.featureNameSupplier = provider.getFeatureNameSupplierForTask(scope, task); |
| } |
| |
| // not used directly, but considered as input for the task. |
| task.versionCode = config::getVersionCode; |
| task.versionName = config::getVersionName; |
| |
| task.variantType = variantType; |
| task.splits = getAbiSplitData(scope); |
| task.outputBaseName = config.getBaseName(); |
| task.applicationId = config::getApplicationId; |
| task.debuggable = config.getBuildType().isDebuggable(); |
| task.aaptOptions = |
| DslAdaptersKt.convert(scope.getGlobalScope().getExtension().getAaptOptions()); |
| Pair<FileCollection, String> aapt2AndVersion = |
| Aapt2MavenUtils.getAapt2FromMavenAndVersion(scope.getGlobalScope()); |
| task.getAapt2FromMaven().from(aapt2AndVersion.getFirst()); |
| task.aapt2Version = aapt2AndVersion.getSecond(); |
| |
| task.androidJarProvider = |
| scope.getGlobalScope().getSdkComponents().getAndroidJarProvider(); |
| |
| task.mergeBlameFolder = scope.getResourceBlameLogDir(); |
| |
| task.manifestMergeBlameFile = |
| scope.getArtifacts() |
| .getFinalProduct(InternalArtifactType.MANIFEST_MERGE_BLAME_FILE); |
| |
| task.errorFormatMode = |
| SyncOptions.getErrorFormatMode(scope.getGlobalScope().getProjectOptions()); |
| |
| // if BASE_FEATURE get the app ID from the app module |
| if (variantType.isBaseModule() && variantType.isHybrid()) { |
| task.applicationIdOverride = |
| scope.getArtifactFileCollection( |
| METADATA_VALUES, PROJECT, METADATA_BASE_MODULE_DECLARATION); |
| } else if (variantType.isFeatureSplit()) { |
| // if feature split, get it from the base module |
| task.applicationIdOverride = |
| scope.getArtifactFileCollection( |
| COMPILE_CLASSPATH, PROJECT, FEATURE_APPLICATION_ID_DECLARATION); |
| } |
| } |
| |
| private static ImmutableSortedMap<String, ApkData> getAbiSplitData( |
| VariantScope variantScope) { |
| return variantScope |
| .getOutputScope() |
| .getApkDatas() |
| .stream() |
| .filter( |
| apk -> |
| apk.isEnabled() |
| && apk.getFilter(VariantOutput.FilterType.ABI) != null) |
| .collect( |
| ImmutableSortedMap.toImmutableSortedMap( |
| Comparator.naturalOrder(), |
| apk -> |
| apk.getFilter(VariantOutput.FilterType.ABI) |
| .getIdentifier(), |
| apk -> apk)); |
| } |
| } |
| } |