diff --git a/README.md b/README.md index ddc51bba..19b16851 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.13.2](https://github.com/google/bundletool/releases) +Latest release: [1.14.0](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index d97657e7..3797761b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.13.2 +release_version = 1.14.0 diff --git a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java index 3943f67c..e89edee6 100644 --- a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java +++ b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java @@ -19,6 +19,7 @@ import com.android.tools.build.bundletool.commands.BuildApksCommand; import com.android.tools.build.bundletool.commands.BuildBundleCommand; import com.android.tools.build.bundletool.commands.BuildSdkApksCommand; +import com.android.tools.build.bundletool.commands.BuildSdkApksForAppCommand; import com.android.tools.build.bundletool.commands.BuildSdkAsarCommand; import com.android.tools.build.bundletool.commands.BuildSdkBundleCommand; import com.android.tools.build.bundletool.commands.CheckTransparencyCommand; @@ -88,6 +89,9 @@ static void main(String[] args, Runtime runtime) { case BuildSdkApksCommand.COMMAND_NAME: BuildSdkApksCommand.fromFlags(flags).execute(); break; + case BuildSdkApksForAppCommand.COMMAND_NAME: + BuildSdkApksForAppCommand.fromFlags(flags).execute(); + break; case BuildSdkAsarCommand.COMMAND_NAME: BuildSdkAsarCommand.fromFlags(flags).execute(); break; @@ -171,6 +175,7 @@ public static void help() { BuildApksCommand.help(), BuildSdkBundleCommand.help(), BuildSdkApksCommand.help(), + BuildSdkApksForAppCommand.help(), BuildSdkAsarCommand.help(), PrintDeviceTargetingConfigCommand.help(), EvaluateDeviceTargetingConfigCommand.help(), @@ -206,6 +211,9 @@ public static void help(String commandName, Runtime runtime) { case BuildSdkApksCommand.COMMAND_NAME: commandHelp = BuildSdkApksCommand.help(); break; + case BuildSdkApksForAppCommand.COMMAND_NAME: + commandHelp = BuildSdkApksForAppCommand.help(); + break; case BuildSdkAsarCommand.COMMAND_NAME: commandHelp = BuildSdkAsarCommand.help(); break; diff --git a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java index b3842894..5acea4c2 100644 --- a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java @@ -27,6 +27,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.MAIN_ACTION_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.META_DATA_GMS_VERSION; import static com.android.tools.build.bundletool.model.AndroidManifest.TOUCHSCREEN_FEATURE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.USES_FEATURE_HARDWARE_WATCH_NAME; import static com.google.common.base.Preconditions.checkNotNull; import com.android.tools.build.bundletool.model.AndroidManifest; @@ -43,11 +44,13 @@ /** Utility methods for creation of archived manifest. */ public final class ArchivedAndroidManifestUtils { public static final String META_DATA_KEY_ARCHIVED = "com.android.vending.archive"; - + public static final int WINDOW_IS_TRANSLUCENT_RESOURCE_ID = 0x01010058; + public static final int WINDOW_BACKGROUND_RESOURCE_ID = 0x01010054; + public static final int SCREEN_BACKGROUND_DARK_TRANSPARENT_THEME_RESOURCE_ID = 0x010800a9; + public static final int HOLO_LIGHT_NO_ACTION_BAR_FULSCREEN_THEME_RESOURCE_ID = 0x010300f1; + public static final int BACKGROUND_DIM_ENABLED = 0x0101021f; public static final String REACTIVATE_ACTIVITY_NAME = "com.google.android.archive.ReactivateActivity"; - public static final int HOLO_LIGHT_NO_ACTION_BAR_FULSCREEN_THEME_RES_ID = 0x010300f1; - public static final String UPDATE_BROADCAST_RECEIVER_NAME = "com.google.android.archive.UpdateBroadcastReceiver"; public static final String MY_PACKAGE_REPLACED_ACTION_NAME = @@ -119,6 +122,11 @@ public static AndroidManifest createArchivedManifest(AndroidManifest manifest) { .getMetadataElement(META_DATA_GMS_VERSION) .ifPresent(editor::addApplicationChildElement); + // Make archived APK generated for wear AAB as a wear app + manifest + .getUsesFeatureElement(USES_FEATURE_HARDWARE_WATCH_NAME) + .forEach(editor::addManifestChildElement); + CHILDREN_ELEMENTS_TO_KEEP.forEach( elementName -> editor.copyChildrenElements(manifest, elementName)); @@ -136,7 +144,7 @@ private static XmlProtoNode createMinimalManifestTag() { .build()); } - public static AndroidManifest updateArchivedIcons( + public static AndroidManifest updateArchivedIconsAndTheme( AndroidManifest manifest, ImmutableMap resourceNameToIdMap) { ManifestEditor archivedManifestEditor = manifest.toEditor(); @@ -151,6 +159,10 @@ public static AndroidManifest updateArchivedIcons( resourceNameToIdMap.get(ARCHIVED_ROUND_ICON_DRAWABLE_NAME)); } + archivedManifestEditor.setActivityTheme( + REACTIVATE_ACTIVITY_NAME, + resourceNameToIdMap.getOrDefault(ArchivedResourcesHelper.ARCHIVED_TV_THEME_NAME, 0)); + return archivedManifestEditor.save(); } @@ -168,10 +180,11 @@ private static Activity createReactivateActivity(AndroidManifest manifest) { return Activity.builder() .setName(REACTIVATE_ACTIVITY_NAME) - .setTheme(HOLO_LIGHT_NO_ACTION_BAR_FULSCREEN_THEME_RES_ID) + .setTheme(HOLO_LIGHT_NO_ACTION_BAR_FULSCREEN_THEME_RESOURCE_ID) .setExported(true) .setExcludeFromRecents(true) .setStateNotNeeded(true) + .setNoHistory(true) .setIntentFilter(intentFilterBuilder.build()) .build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java index 11ff42d7..fd9cd40b 100644 --- a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.android.aapt.Resources.ResourceTable; -import com.android.tools.build.bundletool.commands.BuildApksModule.UpdateIconInArchiveMode; import com.android.tools.build.bundletool.io.ResourceReader; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.AppBundle; @@ -47,13 +46,11 @@ * resources and two custom actions to clear app cache and to wake up an app. */ public final class ArchivedApksGenerator { - private final boolean updateIconInArchiveMode; private final ResourceReader resourceReader; private final ArchivedResourcesHelper archivedResourcesHelper; @Inject - ArchivedApksGenerator(@UpdateIconInArchiveMode boolean updateIconInArchiveMode) { - this.updateIconInArchiveMode = updateIconInArchiveMode; + ArchivedApksGenerator() { resourceReader = new ResourceReader(); archivedResourcesHelper = new ArchivedResourcesHelper(resourceReader); } @@ -74,37 +71,23 @@ public ModuleSplit generateArchivedApk( ResourceInjector resourceInjector = new ResourceInjector(archivedResourceTable.toBuilder(), appBundle.getPackageName()); - ImmutableMap additionalResourcesByByteSource = ImmutableMap.of(); - if (updateIconInArchiveMode) { - ImmutableMap extraResourceNameToIdMap = - ArchivedResourcesHelper.injectExtraResources( - resourceInjector, customAppStorePackageName, iconAttribute, roundIconAttribute); - - additionalResourcesByByteSource = - archivedResourcesHelper.buildAdditionalResourcesByByteSourceMap( - extraResourceNameToIdMap.get(ArchivedResourcesHelper.CLOUD_SYMBOL_DRAWABLE_NAME), - extraResourceNameToIdMap.get(ArchivedResourcesHelper.OPACITY_LAYER_DRAWABLE_NAME), - iconAttribute, - roundIconAttribute, - archivedResourcesHelper.findArchivedClassesDexPath( - appBundle.getVersion(), - BundleTransparencyCheckUtils.isTransparencyEnabled(appBundle))); - - archivedManifest = - ArchivedAndroidManifestUtils.updateArchivedIcons( - archivedManifest, extraResourceNameToIdMap); - } else { - resourceInjector.addStringResource( - ArchivedResourcesHelper.APP_STORE_PACKAGE_NAME_RESOURCE_NAME, - ArchivedResourcesHelper.getAppStorePackageName(customAppStorePackageName)); - additionalResourcesByByteSource = - ImmutableMap.of( - BundleModule.DEX_DIRECTORY.resolve("classes.dex"), - resourceReader.getResourceByteSource( - archivedResourcesHelper.findArchivedClassesDexPath( - appBundle.getVersion(), - BundleTransparencyCheckUtils.isTransparencyEnabled(appBundle)))); - } + ImmutableMap extraResourceNameToIdMap = + ArchivedResourcesHelper.injectExtraResources( + resourceInjector, customAppStorePackageName, iconAttribute, roundIconAttribute); + + ImmutableMap additionalResourcesByByteSource = + archivedResourcesHelper.buildAdditionalResourcesByByteSourceMap( + extraResourceNameToIdMap.get(ArchivedResourcesHelper.CLOUD_SYMBOL_DRAWABLE_NAME), + extraResourceNameToIdMap.get(ArchivedResourcesHelper.OPACITY_LAYER_DRAWABLE_NAME), + iconAttribute, + roundIconAttribute, + archivedResourcesHelper.findArchivedClassesDexPath( + appBundle.getVersion(), + BundleTransparencyCheckUtils.isTransparencyEnabled(appBundle))); + + archivedManifest = + ArchivedAndroidManifestUtils.updateArchivedIconsAndTheme( + archivedManifest, extraResourceNameToIdMap); ModuleSplit moduleSplit = ModuleSplit.forArchive( diff --git a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedResourcesHelper.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedResourcesHelper.java index f0b82514..478b17c0 100644 --- a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedResourcesHelper.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedResourcesHelper.java @@ -16,12 +16,22 @@ package com.android.tools.build.bundletool.archive; +import static com.android.tools.build.bundletool.archive.ArchivedAndroidManifestUtils.BACKGROUND_DIM_ENABLED; +import static com.android.tools.build.bundletool.archive.ArchivedAndroidManifestUtils.HOLO_LIGHT_NO_ACTION_BAR_FULSCREEN_THEME_RESOURCE_ID; +import static com.android.tools.build.bundletool.archive.ArchivedAndroidManifestUtils.SCREEN_BACKGROUND_DARK_TRANSPARENT_THEME_RESOURCE_ID; +import static com.android.tools.build.bundletool.archive.ArchivedAndroidManifestUtils.WINDOW_BACKGROUND_RESOURCE_ID; +import static com.android.tools.build.bundletool.archive.ArchivedAndroidManifestUtils.WINDOW_IS_TRANSLUCENT_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.DRAWABLE_RESOURCE_ID; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Comparator.reverseOrder; +import com.android.aapt.Resources.Item; +import com.android.aapt.Resources.Primitive; +import com.android.aapt.Resources.Reference; +import com.android.aapt.Resources.Style; +import com.android.aapt.Resources.Style.Entry; import com.android.tools.build.bundletool.io.ResourceReader; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ResourceInjector; @@ -39,6 +49,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.util.Optional; +import java.util.function.Consumer; import java.util.logging.Logger; /** Helper methods for managing extra resources for archived apps. */ @@ -56,6 +67,7 @@ public final class ArchivedResourcesHelper { "com_android_vending_archive_application_round_icon"; public static final String ARCHIVED_SPLASH_SCREEN_LAYOUT_NAME = "com_android_vending_archive_splash_screen_layout"; + public static final String ARCHIVED_TV_THEME_NAME = "com_android_vending_archive_tv_theme"; public static final String ARCHIVED_CLASSES_DEX_PATH_PREFIX = "/com/android/tools/build/bundletool/archive/dex"; @@ -156,6 +168,11 @@ public static ImmutableMap injectExtraResources( BundleModule.DRAWABLE_RESOURCE_DIRECTORY .resolve(String.format("%s.xml", CLOUD_SYMBOL_DRAWABLE_NAME)) .toString()) + .getFullResourceId()) + .put( + ARCHIVED_TV_THEME_NAME, + resourceInjector + .addStyleResource(ARCHIVED_TV_THEME_NAME, buildArchivedTvActivityTheme()) .getFullResourceId()); iconAttribute.ifPresent( attribute -> { @@ -283,6 +300,38 @@ private static XmlProtoNode buildFrameLayoutXmlNode(int imageResId) { .asXmlProtoElement()); } + private static Style buildArchivedTvActivityTheme() { + return Style.newBuilder() + .setParent( + Reference.newBuilder().setId(HOLO_LIGHT_NO_ACTION_BAR_FULSCREEN_THEME_RESOURCE_ID)) + // To make the background of the activity transparent. + .addEntry( + createStyleEntryBuilder( + WINDOW_IS_TRANSLUCENT_RESOURCE_ID, + item -> item.setPrim(Primitive.newBuilder().setBooleanValue(true)))) + // A black background. + .addEntry( + createStyleEntryBuilder( + WINDOW_BACKGROUND_RESOURCE_ID, + item -> + item.setRef( + Reference.newBuilder() + .setId(SCREEN_BACKGROUND_DARK_TRANSPARENT_THEME_RESOURCE_ID)))) + // Add a dimmed effect to background. + .addEntry( + createStyleEntryBuilder( + BACKGROUND_DIM_ENABLED, + item -> item.setPrim(Primitive.newBuilder().setBooleanValue(true)))) + .build(); + } + + private static Entry.Builder createStyleEntryBuilder( + int resourceId, Consumer itemConsumer) { + Item.Builder itemBuilder = Item.newBuilder(); + itemConsumer.accept(itemBuilder); + return Entry.newBuilder().setKey(Reference.newBuilder().setId(resourceId)).setItem(itemBuilder); + } + private static XmlProtoAttributeBuilder createDrawableAttribute(int referenceId) { return XmlProtoAttributeBuilder.create("drawable") .setResourceId(DRAWABLE_RESOURCE_ID) diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java index 5dc7ead8..dd40fc33 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java @@ -311,6 +311,8 @@ ListeningExecutorService getExecutorService() { public abstract Optional getAppStorePackageName(); + public abstract boolean getEnableBaseModuleMinSdkAsDefaultTargeting(); + public static Builder builder() { return new AutoValue_BuildApksCommand.Builder() .setOverwriteOutput(false) @@ -326,7 +328,8 @@ public static Builder builder() { .setSystemApkOptions(ImmutableSet.of()) .setEnableApkSerializerWithoutBundleRecompression(true) .setRuntimeEnabledSdkBundlePaths(ImmutableSet.of()) - .setRuntimeEnabledSdkArchivePaths(ImmutableSet.of()); + .setRuntimeEnabledSdkArchivePaths(ImmutableSet.of()) + .setEnableBaseModuleMinSdkAsDefaultTargeting(false); } /** Builder for the {@link BuildApksCommand}. */ @@ -573,6 +576,12 @@ public abstract Builder setLocalDeploymentRuntimeEnabledSdkConfig( */ public abstract Builder setAppStorePackageName(String appStorePackageName); + /** + * If true, will set default min sdk version targeting for generated splits as min sdk of base + * module. + */ + public abstract Builder setEnableBaseModuleMinSdkAsDefaultTargeting(boolean value); + abstract BuildApksCommand autoBuild(); public BuildApksCommand build() { @@ -993,13 +1002,7 @@ private ImmutableMap getValidatedSdkModules( ImmutableMap sdkAsars = getValidatedSdkAsarsByPackageName(closer, tempDir); validateSdkAsarsMatchAppBundleDependencies(appBundle, sdkAsars); return sdkAsars.entrySet().stream() - .collect( - toImmutableMap( - Entry::getKey, - entry -> - entry.getValue().getModule().toBuilder() - .setSdkModulesConfig(entry.getValue().getSdkModulesConfig()) - .build())); + .collect(toImmutableMap(Entry::getKey, entry -> entry.getValue().getModule())); } ImmutableMap sdkBundles = getValidatedSdkBundlesByPackageName(closer, tempDir); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java index be27d9c1..25da71ed 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java @@ -351,6 +351,9 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat apkGenerationConfiguration.setSuffixStrippings(apkOptimizations.getSuffixStrippings()); + apkGenerationConfiguration.setEnableBaseModuleMinSdkAsDefaultTargeting( + command.getEnableBaseModuleMinSdkAsDefaultTargeting()); + command .getMinSdkForAdditionalVariantWithV3Rotation() .ifPresent(apkGenerationConfiguration::setMinSdkForAdditionalVariantWithV3Rotation); @@ -360,6 +363,8 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat private ApkGenerationConfiguration getAssetSliceGenerationConfiguration() { return ApkGenerationConfiguration.builder() + .setEnableBaseModuleMinSdkAsDefaultTargeting( + command.getEnableBaseModuleMinSdkAsDefaultTargeting()) .setOptimizationDimensions(apkOptimizations.getSplitDimensionsForAssetModules()) .setSuffixStrippings(apkOptimizations.getSuffixStrippings()) .build(); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java index 67722ed6..d847b3b2 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksModule.java @@ -125,15 +125,6 @@ static Optional provideOutputPrintStream(BuildApksCommand command) return command.getOutputPrintStream(); } - @CommandScoped - @Provides - @UpdateIconInArchiveMode - static boolean provideUpdateIconInArchiveMode(BuildApksCommand command) { - @SuppressWarnings("unused") - boolean updateIconInArchiveMode = false; - return updateIconInArchiveMode; - } - @CommandScoped @Provides static Optional provideDeviceSpec(BuildApksCommand command) { @@ -207,10 +198,5 @@ static Optional provideLocalRuntimeEnabl @Retention(RUNTIME) public @interface ApkSigningConfigProvider {} - /** Qualifying annotation a {@code boolean} on whether to update archived apps icon. */ - @Qualifier - @Retention(RUNTIME) - public @interface UpdateIconInArchiveMode {} - private BuildApksModule() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommand.java new file mode 100644 index 00000000..a29aeebb --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommand.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2023 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.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.BundleParser.getModulesZip; +import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; +import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileHasExtension; +import static com.google.common.base.Preconditions.checkArgument; + +import com.android.bundle.RuntimeEnabledSdkConfigProto.SdkSplitPropertiesInheritedFromApp; +import com.android.tools.build.bundletool.androidtools.Aapt2Command; +import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; +import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; +import com.android.tools.build.bundletool.flags.Flag; +import com.android.tools.build.bundletool.flags.ParsedFlags; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.Password; +import com.android.tools.build.bundletool.model.SdkAsar; +import com.android.tools.build.bundletool.model.SignerConfig; +import com.android.tools.build.bundletool.model.SigningConfiguration; +import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; +import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.files.BufferedIo; +import com.android.tools.build.bundletool.sdkmodule.SdkModuleToAppBundleModuleConverter; +import com.android.tools.build.bundletool.validation.SdkAsarValidator; +import com.google.auto.value.AutoValue; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.protobuf.util.JsonFormat; +import java.io.IOException; +import java.io.PrintStream; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +/** Command to generate SDK split for an app from a given ASAR. */ +@AutoValue +public abstract class BuildSdkApksForAppCommand { + + private static final int DEFAULT_THREAD_POOL_SIZE = 4; + + public static final String COMMAND_NAME = "build-sdk-apks-for-app"; + + private static final Flag SDK_ARCHIVE_LOCATION_FLAG = Flag.path("sdk-archive"); + private static final Flag INHERITED_APP_PROPERTIES_LOCATION_FLAG = + Flag.path("app-properties"); + + private static final Flag OUTPUT_FILE_FLAG = Flag.path("output"); + + private static final Flag AAPT2_PATH_FLAG = Flag.path("aapt2"); + + // Signing-related flags: should match flags from apksig library. + private static final Flag KEYSTORE_FLAG = Flag.path("ks"); + private static final Flag KEY_ALIAS_FLAG = Flag.string("ks-key-alias"); + private static final Flag KEYSTORE_PASSWORD_FLAG = Flag.password("ks-pass"); + private static final Flag KEY_PASSWORD_FLAG = Flag.password("key-pass"); + + private static final SystemEnvironmentProvider DEFAULT_PROVIDER = + new DefaultSystemEnvironmentProvider(); + + public abstract Path getSdkArchivePath(); + + public abstract SdkSplitPropertiesInheritedFromApp getInheritedAppProperties(); + + public abstract Path getOutputFile(); + + public abstract Optional getAapt2Command(); + + public abstract Optional getSigningConfiguration(); + + ListeningExecutorService getExecutorService() { + return getExecutorServiceInternal(); + } + + abstract ListeningExecutorService getExecutorServiceInternal(); + + abstract boolean isExecutorServiceCreatedByBundleTool(); + + public static BuildSdkApksForAppCommand.Builder builder() { + return new AutoValue_BuildSdkApksForAppCommand.Builder(); + } + + /** Builder for {@link BuildSdkApksForAppCommand}. */ + @AutoValue.Builder + public abstract static class Builder { + + /** Sets the path to the SDK archive file. Must have the extension ".asar". */ + public abstract Builder setSdkArchivePath(Path sdkArchivePath); + + /** Sets the config containing app properties that the SDK split should inherit. */ + abstract Builder setInheritedAppProperties( + SdkSplitPropertiesInheritedFromApp sdkSplitPropertiesInheritedFromApp); + + /** Sets path to a config file containing app properties that the SDK split should inherit. */ + public Builder setInheritedAppProperties(Path inheritedAppProperties) { + return setInheritedAppProperties(parseInheritedAppProperties(inheritedAppProperties)); + } + + /** Path to the output produced by this command. Must have extension ".apks". */ + public abstract Builder setOutputFile(Path outputFile); + + /** Provides a wrapper around the execution of the aapt2 command. */ + public abstract Builder setAapt2Command(Aapt2Command aapt2Command); + + /** Sets the signing configuration to be used for all generated APKs. */ + public abstract Builder setSigningConfiguration(SigningConfiguration signingConfiguration); + + /** + * Allows to set an executor service for parallelization. + * + *

Optional. The caller is responsible for providing a service that accepts new tasks, and + * for shutting it down afterwards. + */ + @CanIgnoreReturnValue + public Builder setExecutorService(ListeningExecutorService executorService) { + setExecutorServiceInternal(executorService); + setExecutorServiceCreatedByBundleTool(false); + return this; + } + + abstract Builder setExecutorServiceInternal(ListeningExecutorService executorService); + + abstract Optional getExecutorServiceInternal(); + + /** + * Sets whether the ExecutorService has been created by bundletool, otherwise provided by the + * client. + * + *

If true, the ExecutorService is shut down at the end of execution of this command. + */ + abstract Builder setExecutorServiceCreatedByBundleTool(boolean value); + + public abstract BuildSdkApksForAppCommand autoBuild(); + + public BuildSdkApksForAppCommand build() { + if (!getExecutorServiceInternal().isPresent()) { + setExecutorServiceInternal(createInternalExecutorService(DEFAULT_THREAD_POOL_SIZE)); + setExecutorServiceCreatedByBundleTool(true); + } + return autoBuild(); + } + } + + public static CommandHelp help() { + return CommandHelp.builder() + .setCommandName(COMMAND_NAME) + .setCommandDescription(CommandDescription.builder().setShortDescription("").build()) + .addFlag( + FlagDescription.builder() + .setFlagName(SDK_ARCHIVE_LOCATION_FLAG.getName()) + .setExampleValue("sdk.asar") + .setDescription("Path to SDK archive to generate app-specific split APKs from.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(INHERITED_APP_PROPERTIES_LOCATION_FLAG.getName()) + .setExampleValue("config.json") + .setDescription( + "Path to the JSON config containing app properties that the SDK split should" + + " inherit.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(OUTPUT_FILE_FLAG.getName()) + .setExampleValue("output.apks") + .setDescription("Path to where the APK Set archive should be created.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(AAPT2_PATH_FLAG.getName()) + .setExampleValue("path/to/aapt2") + .setOptional(true) + .setDescription("Path to the aapt2 binary to use.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEYSTORE_FLAG.getName()) + .setExampleValue("path/to/keystore") + .setOptional(true) + .setDescription( + "Path to the keystore that should be used to sign the generated APKs. If not " + + "set, the default debug keystore will be used if it exists. If not found " + + "the APKs will not be signed. If set, the flag '%s' must also be set.", + KEY_ALIAS_FLAG) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEY_ALIAS_FLAG.getName()) + .setExampleValue("key-alias") + .setOptional(true) + .setDescription( + "Alias of the key to use in the keystore to sign the generated APKs.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEYSTORE_PASSWORD_FLAG.getName()) + .setExampleValue("[pass|file]:value") + .setOptional(true) + .setDescription( + "Password of the keystore to use to sign the generated APKs. If provided, must " + + "be prefixed with either 'pass:' (if the password is passed in clear " + + "text, e.g. 'pass:qwerty') or 'file:' (if the password is the first line " + + "of a file, e.g. 'file:/tmp/myPassword.txt'). If this flag is not set, " + + "the password will be requested on the prompt.") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(KEY_PASSWORD_FLAG.getName()) + .setExampleValue("key-password") + .setOptional(true) + .setDescription( + "Password of the key in the keystore to use to sign the generated APKs. If " + + "provided, must be prefixed with either 'pass:' (if the password is " + + "passed in clear text, e.g. 'pass:qwerty') or 'file:' (if the password " + + "is the first line of a file, e.g. 'file:/tmp/myPassword.txt'). If this " + + "flag is not set, the keystore password will be tried. If that fails, " + + "the password will be requested on the prompt.") + .build()) + .build(); + } + + public static BuildSdkApksForAppCommand fromFlags(ParsedFlags flags) { + return fromFlags(flags, System.out, DEFAULT_PROVIDER); + } + + static BuildSdkApksForAppCommand fromFlags( + ParsedFlags flags, PrintStream out, SystemEnvironmentProvider systemEnvironmentProvider) { + BuildSdkApksForAppCommand.Builder command = + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(SDK_ARCHIVE_LOCATION_FLAG.getRequiredValue(flags)) + .setInheritedAppProperties( + INHERITED_APP_PROPERTIES_LOCATION_FLAG.getRequiredValue(flags)) + .setOutputFile(OUTPUT_FILE_FLAG.getRequiredValue(flags)); + + AAPT2_PATH_FLAG + .getValue(flags) + .ifPresent( + aapt2Path -> command.setAapt2Command(Aapt2Command.createFromExecutablePath(aapt2Path))); + + populateSigningConfigurationFromFlags(command, flags, out, systemEnvironmentProvider); + + return command.build(); + } + + public void execute() { + validateInput(); + + try (TempDirectory tempDir = new TempDirectory(getClass().getSimpleName()); + ZipFile asarZip = new ZipFile(getSdkArchivePath().toFile())) { + Path modulesPath = tempDir.getPath().resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + try (ZipFile modulesZip = getModulesZip(asarZip, modulesPath)) { + SdkAsarValidator.validateModulesFile(modulesZip); + SdkAsar sdkAsar = SdkAsar.buildFromZip(asarZip, modulesZip, modulesPath); + generateAppApks(sdkAsar, tempDir); + } + } catch (ZipException e) { + throw CommandExecutionException.builder() + .withInternalMessage("ASAR is not a valid zip file.") + .withCause(e) + .build(); + } catch (IOException e) { + throw new UncheckedIOException("An error occurred when processing the SDK archive.", e); + } + } + + private void validateInput() { + checkFileExistsAndReadable(getSdkArchivePath()); + checkFileHasExtension("ASAR file", getSdkArchivePath(), ".asar"); + } + + private void generateAppApks(SdkAsar sdkAsar, TempDirectory tempDirectory) { + BundleModule convertedAppModule = + new SdkModuleToAppBundleModuleConverter(sdkAsar.getModule(), getInheritedAppProperties()) + .convert(); + DaggerBuildSdkApksForAppManagerComponent.builder() + .setBuildSdkApksForAppCommand(this) + .setModule(convertedAppModule) + .setTempDirectory(tempDirectory) + .build() + .create() + .execute(); + } + + private static SdkSplitPropertiesInheritedFromApp parseInheritedAppProperties( + Path propertiesInheritedFromAppFile) { + checkFileExistsAndReadable(propertiesInheritedFromAppFile); + checkFileHasExtension( + "JSON file containing properties inherited from the app", + propertiesInheritedFromAppFile, + ".json"); + try (Reader reader = BufferedIo.reader(propertiesInheritedFromAppFile)) { + SdkSplitPropertiesInheritedFromApp.Builder builder = + SdkSplitPropertiesInheritedFromApp.newBuilder(); + JsonFormat.parser().merge(reader, builder); + return builder.build(); + } catch (IOException e) { + throw new UncheckedIOException( + String.format("Error while reading the file '%s'.", propertiesInheritedFromAppFile), e); + } + } + + private static void populateSigningConfigurationFromFlags( + Builder buildSdkApksForAppCommand, + ParsedFlags flags, + PrintStream out, + SystemEnvironmentProvider provider) { + // Signing-related arguments. + Optional keystorePath = KEYSTORE_FLAG.getValue(flags); + Optional keyAlias = KEY_ALIAS_FLAG.getValue(flags); + Optional keystorePassword = KEYSTORE_PASSWORD_FLAG.getValue(flags); + Optional keyPassword = KEY_PASSWORD_FLAG.getValue(flags); + + if (keystorePath.isPresent() && keyAlias.isPresent()) { + SignerConfig signerConfig = + SignerConfig.extractFromKeystore( + keystorePath.get(), keyAlias.get(), keystorePassword, keyPassword); + SigningConfiguration.Builder builder = + SigningConfiguration.builder().setSignerConfig(signerConfig); + buildSdkApksForAppCommand.setSigningConfiguration(builder.build()); + } else if (keystorePath.isPresent() && !keyAlias.isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage("Flag --ks-key-alias is required when --ks is set.") + .build(); + } else if (!keystorePath.isPresent() && keyAlias.isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage("Flag --ks is required when --ks-key-alias is set.") + .build(); + } else { + // Try to use debug keystore if present. + Optional debugConfig = + DebugKeystoreUtils.getDebugSigningConfiguration(provider); + if (debugConfig.isPresent()) { + out.printf( + "INFO: The APKs will be signed with the debug keystore found at '%s'.%n", + DebugKeystoreUtils.DEBUG_KEYSTORE_CACHE.getUnchecked(provider).get()); + buildSdkApksForAppCommand.setSigningConfiguration(debugConfig.get()); + } else { + out.println( + "WARNING: The APKs won't be signed and thus not installable unless you also pass a " + + "keystore via the flag --ks. See the command help for more information."); + } + } + } + + /** + * Creates an internal executor service that uses at most the given number of threads. + * + *

The caller is responsible for shutting down the executor service. + */ + private static ListeningExecutorService createInternalExecutorService(int maxThreads) { + checkArgument(maxThreads >= 0, "The maxThreads must be positive, got %s.", maxThreads); + return MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(maxThreads)); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManager.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManager.java new file mode 100644 index 00000000..d7ff7cf0 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 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.tools.build.bundletool.commands; + +import com.android.bundle.Commands.LocalTestingInfo; +import com.android.tools.build.bundletool.io.ApkSerializerManager; +import com.android.tools.build.bundletool.io.ApkSetWriter; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.GeneratedApks; +import com.android.tools.build.bundletool.model.GeneratedAssetSlices; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.shards.ModuleSplitterForShards; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.Optional; +import javax.inject.Inject; + +/** Executes build-sdk-apks-for-app command. */ +public class BuildSdkApksForAppManager { + + private final BuildSdkApksForAppCommand command; + private final BundleModule module; + private final ModuleSplitterForShards moduleSplitterForShards; + private final TempDirectory tempDirectory; + private final ApkSerializerManager apkSerializerManager; + + @Inject + BuildSdkApksForAppManager( + BuildSdkApksForAppCommand command, + BundleModule module, + ModuleSplitterForShards moduleSplitterForShards, + TempDirectory tempDirectory, + ApkSerializerManager apkSerializerManager) { + this.command = command; + this.module = module; + this.moduleSplitterForShards = moduleSplitterForShards; + this.tempDirectory = tempDirectory; + this.apkSerializerManager = apkSerializerManager; + } + + void execute() { + // No sharding dimensions are passed to module splitter, so the resulting list will only have 1 + // element. + ImmutableList splits = + moduleSplitterForShards.generateSplits(module, /* shardingDimensions= */ ImmutableSet.of()); + + GeneratedApks generatedApks = GeneratedApks.fromModuleSplits(splits); + ApkSetWriter apkSetWriter = ApkSetWriter.zip(tempDirectory.getPath(), command.getOutputFile()); + + apkSerializerManager.serializeApkSetWithoutToc( + apkSetWriter, + generatedApks, + GeneratedAssetSlices.builder().build(), + /* deviceSpec= */ Optional.empty(), + LocalTestingInfo.getDefaultInstance(), + /* permanentlyFusedModules= */ ImmutableSet.of()); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManagerComponent.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManagerComponent.java new file mode 100644 index 00000000..1e40f5b1 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManagerComponent.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 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.tools.build.bundletool.commands; + +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.model.BundleModule; +import dagger.BindsInstance; +import dagger.Component; + +/** Dagger component to create {@link BuildSdkApksForAppManager}. */ +@Component(modules = {BuildSdkApksForAppModule.class}) +public interface BuildSdkApksForAppManagerComponent { + BuildSdkApksForAppManager create(); + + /** Builder for {@link BuildSdkApksForAppManagerComponent}. */ + @Component.Builder + interface Builder { + BuildSdkApksForAppManagerComponent build(); + + @BindsInstance + Builder setBuildSdkApksForAppCommand(BuildSdkApksForAppCommand command); + + @BindsInstance + Builder setModule(BundleModule module); + + @BindsInstance + Builder setTempDirectory(TempDirectory tempDirectory); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppModule.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppModule.java new file mode 100644 index 00000000..79348159 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppModule.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2023 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.tools.build.bundletool.commands; + +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.Config.Bundletool; +import com.android.bundle.Devices.DeviceSpec; +import com.android.tools.build.bundletool.androidtools.Aapt2Command; +import com.android.tools.build.bundletool.androidtools.P7ZipCommand; +import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; +import com.android.tools.build.bundletool.io.ApkSerializer; +import com.android.tools.build.bundletool.io.ModuleSplitSerializer; +import com.android.tools.build.bundletool.io.TempDirectory; +import com.android.tools.build.bundletool.model.ApkListener; +import com.android.tools.build.bundletool.model.ApkModifier; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.Bundle; +import com.android.tools.build.bundletool.model.BundleMetadata; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.DefaultSigningConfigurationProvider; +import com.android.tools.build.bundletool.model.SigningConfigurationProvider; +import com.android.tools.build.bundletool.model.SourceStamp; +import com.android.tools.build.bundletool.model.version.BundleToolVersion; +import com.android.tools.build.bundletool.model.version.Version; +import com.android.tools.build.bundletool.optimizations.ApkOptimizations; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListeningExecutorService; +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import java.util.Optional; + +/** Dagger module for build-sdk-apks-for-app command. */ +@Module +public abstract class BuildSdkApksForAppModule { + + @Provides + static Aapt2Command provideAapt2Command( + BuildSdkApksForAppCommand command, TempDirectory tempDir) { + return command + .getAapt2Command() + .orElseGet(() -> CommandUtils.extractAapt2FromJar(tempDir.getPath())); + } + + @Provides + static BundleConfig provideBundleConfig() { + return BundleConfig.newBuilder() + .setBundletool( + Bundletool.newBuilder().setVersion(BundleToolVersion.getCurrentVersion().toString())) + .build(); + } + + @Provides + static Version provideBundletoolVersion() { + return BundleToolVersion.getCurrentVersion(); + } + + @Provides + static Optional provideDeviceSpec() { + return Optional.empty(); + } + + @Provides + static ApkOptimizations provideApkOptimizations() { + return ApkOptimizations.getOptimizationsForUniversalApk(); + } + + @Provides + static BuildApksCommand.ApkBuildMode provideApkBuildMode() { + return ApkBuildMode.DEFAULT; + } + + @Provides + @BuildApksModule.ApkSigningConfigProvider + static Optional provideApkSigningConfigurationProvider( + BuildSdkApksForAppCommand command, Version version) { + return command + .getSigningConfiguration() + .map(signingConfig -> new DefaultSigningConfigurationProvider(signingConfig, version)); + } + + @Provides + static ListeningExecutorService provideExecutorService(BuildSdkApksForAppCommand command) { + return command.getExecutorService(); + } + + @Provides + static Optional provideApkListener() { + return Optional.empty(); + } + + @Provides + static Optional provideApkModifier() { + return Optional.empty(); + } + + @Provides + @BuildApksModule.VerboseLogs + static boolean provideVerbose() { + return false; + } + + @Provides + static Optional provideSourceStamp() { + return Optional.empty(); + } + + @Provides + static Optional privideP7ZipCommand() { + return Optional.empty(); + } + + @BuildApksModule.FirstVariantNumber + @Provides + static Optional provideFirstVariantNumber() { + return Optional.empty(); + } + + @Provides + static Bundle provideBundle(BundleModule module, BundleConfig bundleConfig) { + return AppBundle.buildFromModules( + ImmutableList.of(module), bundleConfig, BundleMetadata.builder().build()) + .toBuilder() + .setPackageNameOptional(module.getAndroidManifest().getPackageName()) + .build(); + } + + @Binds + abstract ApkSerializer apkSerializerHelper(ModuleSplitSerializer apkSerializerHelper); +} diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java index dd1c6b09..bd15fce4 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java @@ -52,7 +52,6 @@ import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; import com.android.tools.build.bundletool.commands.BuildApksModule.FirstVariantNumber; -import com.android.tools.build.bundletool.commands.BuildApksModule.VerboseLogs; import com.android.tools.build.bundletool.device.ApkMatcher; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.ApkModifier; @@ -108,7 +107,6 @@ public ApkSerializerManager( Bundle bundle, Optional apkModifier, @FirstVariantNumber Optional firstVariantNumber, - @VerboseLogs boolean verbose, ApkBuildMode apkBuildMode, ApkPathManager apkPathManager, ApkOptimizations apkOptimizations, @@ -146,6 +144,29 @@ public BuildApksResult serializeApkSet( } } + /** Serialize App Bundle APKs without including TOC in the output archive. */ + public void serializeApkSetWithoutToc( + ApkSetWriter apkSetWriter, + GeneratedApks generatedApks, + GeneratedAssetSlices generatedAssetSlices, + Optional deviceSpec, + LocalTestingInfo localTestingInfo, + ImmutableSet permanentlyFusedModules) { + try { + BuildApksResult toc = + serializeApkSetContent( + apkSetWriter.getSplitsDirectory(), + generatedApks, + generatedAssetSlices, + deviceSpec, + localTestingInfo, + permanentlyFusedModules); + apkSetWriter.writeApkSetWithoutToc(toc); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + /** Serialize SDK Bundle APKs. */ public void serializeSdkApkSet(ApkSetWriter apkSetWriter, GeneratedApks generatedApks) { try { diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java index 8832a569..6d22547c 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSetWriter.java @@ -38,6 +38,8 @@ public interface ApkSetWriter { void writeApkSet(BuildApksResult toc) throws IOException; + void writeApkSetWithoutToc(BuildApksResult toc) throws IOException; + void writeApkSet(BuildSdkApksResult toc) throws IOException; @@ -54,6 +56,11 @@ public void writeApkSet(BuildApksResult toc) throws IOException { Files.write(getSplitsDirectory().resolve(TABLE_OF_CONTENTS_FILE), toc.toByteArray()); } + @Override + public void writeApkSetWithoutToc(BuildApksResult toc) { + // No-op for directory APK set writer. + } + @Override public void writeApkSet(BuildSdkApksResult toc) throws IOException { Files.write(getSplitsDirectory().resolve(TABLE_OF_CONTENTS_FILE), toc.toByteArray()); @@ -72,21 +79,12 @@ public Path getSplitsDirectory() { @Override public void writeApkSet(BuildApksResult toc) throws IOException { - Stream apks = - toc.getVariantList().stream() - .flatMap(variant -> variant.getApkSetList().stream()) - .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()); - Stream assets = - toc.getAssetSliceSetList().stream() - .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream()); - - ImmutableSet apkRelativePaths = - Stream.concat(apks, assets) - .map(ApkDescription::getPath) - .sorted() - .collect(toImmutableSet()); + zipApkSet(getApkRelativePaths(toc), toc.toByteArray()); + } - zipApkSet(apkRelativePaths, toc.toByteArray()); + @Override + public void writeApkSetWithoutToc(BuildApksResult toc) throws IOException { + zipApkSet(getApkRelativePaths(toc), toc.toByteArray(), /* serializeToc= */ false); } @Override @@ -105,9 +103,17 @@ public void writeApkSet(BuildSdkApksResult toc) throws IOException { private void zipApkSet(ImmutableSet apkRelativePaths, byte[] tocBytes) throws IOException { + zipApkSet(apkRelativePaths, tocBytes, /* serializeToc= */ true); + } + + private void zipApkSet( + ImmutableSet apkRelativePaths, byte[] tocBytes, boolean serializeToc) + throws IOException { try (ZipArchive zipArchive = new ZipArchive(outputFile)) { - zipArchive.add( - new BytesSource(tocBytes, TABLE_OF_CONTENTS_FILE, Deflater.NO_COMPRESSION)); + if (serializeToc) { + zipArchive.add( + new BytesSource(tocBytes, TABLE_OF_CONTENTS_FILE, Deflater.NO_COMPRESSION)); + } for (String relativePath : apkRelativePaths) { zipArchive.add( @@ -119,6 +125,21 @@ private void zipApkSet(ImmutableSet apkRelativePaths, byte[] tocBytes) } } } + + private ImmutableSet getApkRelativePaths(BuildApksResult toc) { + Stream apks = + toc.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()); + Stream assets = + toc.getAssetSliceSetList().stream() + .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream()); + + return Stream.concat(apks, assets) + .map(ApkDescription::getPath) + .sorted() + .collect(toImmutableSet()); + } }; } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java index 06def8be..36fa8267 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java @@ -94,6 +94,7 @@ public abstract class AndroidManifest { public static final String INSTALL_TIME_ELEMENT_NAME = "install-time"; public static final String REMOVABLE_ELEMENT_NAME = "removable"; public static final String FUSING_ELEMENT_NAME = "fusing"; + public static final String STYLE_ELEMENT_NAME = "style"; public static final String DEBUGGABLE_ATTRIBUTE_NAME = "debuggable"; public static final String EXTRACT_NATIVE_LIBS_ATTRIBUTE_NAME = "extractNativeLibs"; @@ -238,6 +239,7 @@ public abstract class AndroidManifest { public static final String META_DATA_KEY_SPLITS_REQUIRED = "com.android.vending.splits.required"; public static final String META_DATA_GMS_VERSION = "com.google.android.gms.version"; + public static final String USES_FEATURE_HARDWARE_WATCH_NAME = "android.hardware.type.watch"; public static final String MAIN_ACTION_NAME = "android.intent.action.MAIN"; public static final String LAUNCHER_CATEGORY_NAME = "android.intent.category.LAUNCHER"; @@ -749,6 +751,20 @@ public Optional getMetadataElement(String name) { } } + /** Returns the XML elements with the given "android:name" value. */ + public ImmutableList getUsesFeatureElement(String name) { + return getManifestElement() + .getChildrenElements(USES_FEATURE_ELEMENT_NAME) + .filter( + metadataElement -> + metadataElement + .getAndroidAttribute(NAME_RESOURCE_ID) + .map(XmlProtoAttribute::getValueAsString) + .orElse("") + .equals(name)) + .collect(toImmutableList()); + } + public ModuleDeliveryType getModuleDeliveryType() { if (getManifestDeliveryElement().isPresent()) { ManifestDeliveryElement manifestDeliveryElement = getManifestDeliveryElement().get(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java index f4fbd026..2d4707f7 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java @@ -147,6 +147,8 @@ public static AppBundle buildFromModules( @Override public abstract BundleMetadata getBundleMetadata(); + abstract Optional getPackageNameOptional(); + /** * Returns runtime-enabled SDK dependencies of this bundle, keyed by SDK package name. * @@ -183,6 +185,9 @@ public boolean hasBaseModule() { @Override public String getPackageName() { + if (getPackageNameOptional().isPresent()) { + return getPackageNameOptional().get(); + } if (isAssetOnly()) { return getModules().values().stream() .map(module -> module.getAndroidManifest().getPackageName()) @@ -316,6 +321,12 @@ public Builder addRawModule(BundleModule bundleModule) { public abstract Builder setRuntimeEnabledSdkDependencies( ImmutableMap runtimeEnabledSdkDependencies); + /** + * Package name is explicitly set in BuidSdkApksForAppCommand, since it does not take app bundle + * as input. + */ + public abstract Builder setPackageNameOptional(String packageName); + public abstract AppBundle build(); abstract ImmutableMap.Builder modulesBuilder(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java index cbe41b5f..7c555190 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java @@ -65,6 +65,8 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.TOOLS_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_FEATURE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_SDK_ELEMENT_NAME; @@ -386,6 +388,16 @@ public ManifestEditor addActivity(Activity activity) { return this; } + @CanIgnoreReturnValue + public ManifestEditor setActivityTheme(String activityName, int themeResId) { + manifestElement + .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) + .getOrCreateChildElement(ACTIVITY_ELEMENT_NAME) + .getOrCreateAndroidAttribute(THEME_ATTRIBUTE_NAME, THEME_RESOURCE_ID) + .setValueAsRefId(themeResId); + return this; + } + @CanIgnoreReturnValue public ManifestEditor addReceiver(Receiver receiver) { manifestElement @@ -560,6 +572,12 @@ public ManifestEditor addUsesFeatureElement(String featureName, boolean isRequir return this; } + @CanIgnoreReturnValue + public ManifestEditor addManifestChildElement(XmlProtoElement element) { + manifestElement.addChildElement(element.toBuilder()); + return this; + } + /** * Copies an android attribute with resource id {@code attrResourceId} from 'manifest' element of * android manifest {@code from} into current. diff --git a/src/main/java/com/android/tools/build/bundletool/model/ResourceInjector.java b/src/main/java/com/android/tools/build/bundletool/model/ResourceInjector.java index eeca17c6..f08e9b99 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ResourceInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ResourceInjector.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.model; import com.android.aapt.Resources; +import com.android.aapt.Resources.CompoundValue; import com.android.aapt.Resources.ConfigValue; import com.android.aapt.Resources.Entry; import com.android.aapt.Resources.EntryId; @@ -25,6 +26,7 @@ import com.android.aapt.Resources.Package; import com.android.aapt.Resources.PackageId; import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.Style; import com.android.aapt.Resources.Type; import com.android.aapt.Resources.TypeId; import com.android.aapt.Resources.Value; @@ -48,6 +50,7 @@ public class ResourceInjector { private static final String STRING_ENTRY_TYPE = "string"; private static final String DRAWABLE_ENTRY_TYPE = "drawable"; private static final String LAYOUT_ENTRY_TYPE = "layout"; + private static final String STYLE_ENTRY_TYPE = "style"; private final ResourceTable.Builder resourceTable; private final String packageName; @@ -117,6 +120,19 @@ public ResourceId addLayoutResource(String layoutName, String fileReference) { return addResource(LAYOUT_ENTRY_TYPE, layoutEntry); } + public ResourceId addStyleResource(String styleName, Style style) { + Entry layoutEntry = + Entry.newBuilder() + .setName(styleName) + .addConfigValue( + ConfigValue.newBuilder() + .setValue( + Value.newBuilder() + .setCompoundValue(CompoundValue.newBuilder().setStyle(style)))) + .build(); + return addResource(STYLE_ENTRY_TYPE, layoutEntry); + } + public ResourceId addResource(String entryType, Entry entry) { ResourceId.Builder resourceIdBuilder = ResourceId.builder(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java b/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java index d18aa57b..18070689 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java +++ b/src/main/java/com/android/tools/build/bundletool/model/SdkAsar.java @@ -50,17 +50,21 @@ public abstract class SdkAsar { public static SdkAsar buildFromZip(ZipFile asar, ZipFile modulesFile, Path modulesFilePath) { SdkModulesConfig sdkModulesConfig = readSdkModulesConfig(modulesFile); + BundleModule sdkModule = + Iterables.getOnlyElement( + sanitize( + extractModules( + modulesFile, + BundleType.REGULAR, + Version.of(sdkModulesConfig.getBundletool().getVersion()), + /* apexConfig= */ Optional.empty(), + /* nonModuleDirectories= */ ImmutableSet.of()))) + .toBuilder() + .setSdkModulesConfig(sdkModulesConfig) + .build(); SdkAsar.Builder sdkAsarBuilder = builder() - .setModule( - Iterables.getOnlyElement( - sanitize( - extractModules( - modulesFile, - BundleType.REGULAR, - Version.of(sdkModulesConfig.getBundletool().getVersion()), - /* apexConfig= */ Optional.empty(), - /* nonModuleDirectories= */ ImmutableSet.of())))) + .setModule(sdkModule) .setSdkModulesConfig(sdkModulesConfig) .setModulesFile(modulesFilePath.toFile()) .setSdkMetadata(readSdkMetadata(asar)); diff --git a/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Activity.java b/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Activity.java index 95d4ca27..48856315 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Activity.java +++ b/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Activity.java @@ -42,9 +42,11 @@ public abstract class Activity { public static final String EXCLUDE_FROM_RECENTS_ELEMENT_NAME = "excludeFromRecents"; public static final String STATE_NOT_NEEDED_ELEMENT_NAME = "stateNotNeeded"; + public static final String NO_HISTORY_ELEMENT_NAME = "noHistory"; public static final int EXCLUDE_FROM_RECENTS_RESOURCE_ID = 0x01010017; public static final int STATE_NOT_NEEDED_RESOURCE_ID = 0x01010016; + public static final int NO_HISTORY_RESOURCE_ID = 0x0101022d; abstract Optional getName(); @@ -56,6 +58,8 @@ public abstract class Activity { abstract Optional getStateNotNeeded(); + abstract Optional getNoHistory(); + abstract Optional getIntentFilter(); public static Builder builder() { @@ -70,6 +74,7 @@ public XmlProtoElement asXmlProtoElement() { setExportedAttribute(elementBuilder); setExcludeFromRecentsAttribute(elementBuilder); setStateNotNeeded(elementBuilder); + setNoHistory(elementBuilder); setIntentFilterElement(elementBuilder); return elementBuilder.build(); } @@ -115,6 +120,14 @@ private void setStateNotNeeded(XmlProtoElementBuilder elementBuilder) { } } + private void setNoHistory(XmlProtoElementBuilder elementBuilder) { + if (getNoHistory().isPresent()) { + elementBuilder + .getOrCreateAndroidAttribute(NO_HISTORY_ELEMENT_NAME, NO_HISTORY_RESOURCE_ID) + .setValueAsBoolean(getNoHistory().get()); + } + } + private void setIntentFilterElement(XmlProtoElementBuilder elementBuilder) { if (getIntentFilter().isPresent()) { elementBuilder.addChildElement(getIntentFilter().get().asXmlProtoElement().toBuilder()); @@ -134,6 +147,8 @@ public abstract static class Builder { public abstract Builder setStateNotNeeded(boolean stateNotNeeded); + public abstract Builder setNoHistory(boolean noHistory); + public abstract Builder setIntentFilter(IntentFilter intentFilter); public abstract Activity build(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java index 04b7cbbb..1db56649 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/BundleToolVersion.java @@ -26,7 +26,7 @@ */ public final class BundleToolVersion { - private static final String CURRENT_VERSION = "1.13.2"; + private static final String CURRENT_VERSION = "1.14.0"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java b/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java index ada84aba..94f001ff 100644 --- a/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java +++ b/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java @@ -18,7 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static java.nio.charset.StandardCharsets.UTF_8; -import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.SdkSplitPropertiesInheritedFromApp; import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleEntry; @@ -82,14 +82,15 @@ public final class DexAndResourceRepackager { private static final String ASSETS_SUBDIRECTORY_PREFIX = "RuntimeEnabledSdk-"; private final SdkModulesConfig sdkModulesConfig; - private final RuntimeEnabledSdk sdkDependencyConfig; + private final SdkSplitPropertiesInheritedFromApp inheritedAppProperties; private final DexRepackager dexRepackager; private final JavaResourceRepackager javaResourceRepackager; DexAndResourceRepackager( - SdkModulesConfig sdkModulesConfig, RuntimeEnabledSdk sdkDependencyConfig) { + SdkModulesConfig sdkModulesConfig, + SdkSplitPropertiesInheritedFromApp inheritedAppProperties) { this.sdkModulesConfig = sdkModulesConfig; - this.sdkDependencyConfig = sdkDependencyConfig; + this.inheritedAppProperties = inheritedAppProperties; this.dexRepackager = new DexRepackager(sdkModulesConfig); this.javaResourceRepackager = new JavaResourceRepackager(sdkModulesConfig); } @@ -169,7 +170,7 @@ private void appendResourcesPackageIdElement( Element resourceIdRemappingElement, Document xmlFactory) { Element resourcesPackageIdElement = xmlFactory.createElement(RESOURCES_PACKAGE_ID_ELEMENT_NAME); resourcesPackageIdElement.setTextContent( - Integer.toString(sdkDependencyConfig.getResourcesPackageId())); + Integer.toString(inheritedAppProperties.getResourcesPackageId())); resourceIdRemappingElement.appendChild(resourcesPackageIdElement); } diff --git a/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java b/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java index 0dc8d311..b68374ef 100644 --- a/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java +++ b/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.sdkmodule; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.SdkSplitPropertiesInheritedFromApp; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; @@ -34,21 +35,33 @@ public final class SdkModuleToAppBundleModuleConverter { private final XmlPackageIdRemapper xmlPackageIdRemapper; private final DexAndResourceRepackager dexAndResourceRepackager; private final AndroidResourceRenamer androidResourceRenamer; - private final AndroidManifest appBaseModuleManifest; + private final SdkSplitPropertiesInheritedFromApp inheritedAppProperties; public SdkModuleToAppBundleModuleConverter( BundleModule sdkModule, RuntimeEnabledSdk sdkDependencyConfig, AndroidManifest appBaseModuleManifest) { + this( + sdkModule, + SdkSplitPropertiesInheritedFromApp.newBuilder() + .setPackageName(appBaseModuleManifest.getPackageName()) + .setVersionCode(appBaseModuleManifest.getVersionCode().get()) + .setMinSdkVersion(appBaseModuleManifest.getMinSdkVersion().get()) + .setResourcesPackageId(sdkDependencyConfig.getResourcesPackageId()) + .build()); + } + + public SdkModuleToAppBundleModuleConverter( + BundleModule sdkModule, SdkSplitPropertiesInheritedFromApp inheritedAppProperties) { this.sdkModule = sdkModule; this.resourceTablePackageIdRemapper = - new ResourceTablePackageIdRemapper(sdkDependencyConfig.getResourcesPackageId()); + new ResourceTablePackageIdRemapper(inheritedAppProperties.getResourcesPackageId()); this.xmlPackageIdRemapper = - new XmlPackageIdRemapper(sdkDependencyConfig.getResourcesPackageId()); + new XmlPackageIdRemapper(inheritedAppProperties.getResourcesPackageId()); this.dexAndResourceRepackager = - new DexAndResourceRepackager(sdkModule.getSdkModulesConfig().get(), sdkDependencyConfig); + new DexAndResourceRepackager(sdkModule.getSdkModulesConfig().get(), inheritedAppProperties); this.androidResourceRenamer = new AndroidResourceRenamer(sdkModule.getSdkModulesConfig().get()); - this.appBaseModuleManifest = appBaseModuleManifest; + this.inheritedAppProperties = inheritedAppProperties; } /** @@ -90,10 +103,10 @@ private BundleModule convertNameTypeAndManifest(BundleModule module) { module .getAndroidManifest() .toEditor() - .setPackage(appBaseModuleManifest.getPackageName()) - .setVersionCode(appBaseModuleManifest.getVersionCode().get()) + .setPackage(inheritedAppProperties.getPackageName()) + .setVersionCode(inheritedAppProperties.getVersionCode()) .removeUsesSdkElement() - .setMinSdkVersion(appBaseModuleManifest.getMinSdkVersion().get()) + .setMinSdkVersion(inheritedAppProperties.getMinSdkVersion()) .setHasCode(false) .setSplitIdForFeatureSplit(sdkModuleName) .setDeliveryOptionsForRuntimeEnabledSdkModule() diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java index da7fa0f1..5233a10a 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java @@ -47,6 +47,8 @@ public abstract class ApkGenerationConfiguration { public abstract boolean isInstallableOnExternalStorage(); + public abstract boolean getEnableBaseModuleMinSdkAsDefaultTargeting(); + /** * Returns a list of ABIs for placeholder libraries that should be populated for base modules * without native code. See {@link AbiPlaceholderInjector} for details. @@ -88,6 +90,7 @@ public static Builder builder() { .setDexCompressionSplitterForTargetSdk(UncompressedDexTargetSdk.UNSPECIFIED) .setEnableSparseEncodingVariant(false) .setInstallableOnExternalStorage(false) + .setEnableBaseModuleMinSdkAsDefaultTargeting(false) .setAbisForPlaceholderLibs(ImmutableSet.of()) .setOptimizationDimensions(ImmutableSet.of()) .setMasterPinnedResourceIds(ImmutableSet.of()) @@ -135,6 +138,9 @@ public abstract Builder setSuffixStrippings( public abstract Builder setMinSdkForAdditionalVariantWithV3Rotation( int minSdkForAdditionalVariantWithV3Rotation); + public abstract Builder setEnableBaseModuleMinSdkAsDefaultTargeting( + boolean enableBaseModuleMinSdkAsDefaultTargeting); + public abstract ApkGenerationConfiguration build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java index 6ff992bd..e249170f 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitter.java @@ -20,10 +20,11 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; -import static java.lang.Math.max; +import static com.google.common.primitives.Ints.max; import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.SdkVersionTargeting; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ModuleDeliveryType; import com.android.tools.build.bundletool.model.ModuleSplit; @@ -36,12 +37,16 @@ public class AssetModuleSplitter { private final BundleModule module; private final ApkGenerationConfiguration apkGenerationConfiguration; + private final AppBundle appBundle; private final SuffixManager suffixManager = new SuffixManager(); public AssetModuleSplitter( - BundleModule module, ApkGenerationConfiguration apkGenerationConfiguration) { + BundleModule module, + ApkGenerationConfiguration apkGenerationConfiguration, + AppBundle appBundle) { this.module = checkNotNull(module); this.apkGenerationConfiguration = checkNotNull(apkGenerationConfiguration); + this.appBundle = checkNotNull(appBundle); } public ImmutableList splitModule() { @@ -53,6 +58,11 @@ public ImmutableList splitModule() { ImmutableList splits = splitsBuilder.build(); if (module.getDeliveryType().equals(ModuleDeliveryType.ALWAYS_INITIAL_INSTALL)) { + int baseModuleMinSdk = + apkGenerationConfiguration.getEnableBaseModuleMinSdkAsDefaultTargeting() + && appBundle.hasBaseModule() + ? appBundle.getBaseModule().getAndroidManifest().getEffectiveMinSdkVersion() + : 1; int masterSplitMinSdk = splits.stream() .filter(ModuleSplit::isMasterSplit) @@ -61,7 +71,7 @@ public ImmutableList splitModule() { .orElse(1); splits = splits.stream() - .map(split -> addDefaultSdkApkTargeting(split, masterSplitMinSdk)) + .map(split -> addDefaultSdkApkTargeting(split, masterSplitMinSdk, baseModuleMinSdk)) .collect(toImmutableList()); } return splits.stream().map(this::setAssetSliceManifest).collect(toImmutableList()); @@ -96,7 +106,8 @@ private SplittingPipeline createAssetsSplittingPipeline() { return new SplittingPipeline(assetsSplitters.build()); } - private static ModuleSplit addDefaultSdkApkTargeting(ModuleSplit split, int masterSplitMinSdk) { + private static ModuleSplit addDefaultSdkApkTargeting( + ModuleSplit split, int masterSplitMinSdk, int baseModuleMinSdk) { if (split.getApkTargeting().hasSdkVersionTargeting()) { checkState( split.getApkTargeting().getSdkVersionTargeting().getValue(0).getMin().getValue() @@ -105,7 +116,7 @@ private static ModuleSplit addDefaultSdkApkTargeting(ModuleSplit split, int mast return split; } - int defaultSdkVersion = max(masterSplitMinSdk, ANDROID_L_API_VERSION); + int defaultSdkVersion = max(masterSplitMinSdk, baseModuleMinSdk, ANDROID_L_API_VERSION); return split.toBuilder() .setApkTargeting( split.getApkTargeting().toBuilder() diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java index b39ca3a0..14602761 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/AssetSlicesGenerator.java @@ -55,7 +55,7 @@ public ImmutableList generateAssetSlices() { for (BundleModule module : appBundle.getAssetModules().values()) { AssetModuleSplitter moduleSplitter = - new AssetModuleSplitter(module, apkGenerationConfiguration); + new AssetModuleSplitter(module, apkGenerationConfiguration, appBundle); splits.addAll( moduleSplitter.splitModule().stream() .map( diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java index 70a495f1..014595c9 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ModuleSplitter.java @@ -25,10 +25,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; -import static java.lang.Math.max; +import static com.google.common.primitives.Ints.max; import com.android.aapt.ConfigurationOuterClass.Configuration; -import com.android.bundle.Config.BundleConfig; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.SdkVersionTargeting; @@ -36,7 +35,6 @@ import com.android.tools.build.bundletool.mergers.SameTargetingMerger; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.AppBundle; -import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ManifestEditor; import com.android.tools.build.bundletool.model.ManifestMutator; @@ -86,14 +84,12 @@ public class ModuleSplitter { private final RuntimeEnabledSdkTableInjector runtimeEnabledSdkTableInjector; @VisibleForTesting - public static ModuleSplitter createForTest(BundleModule module, Version bundleVersion) { + public static ModuleSplitter createForTest( + BundleModule module, AppBundle appBundle, Version bundleVersion) { return new ModuleSplitter( module, bundleVersion, - AppBundle.buildFromModules( - ImmutableList.of(module), - BundleConfig.getDefaultInstance(), - BundleMetadata.builder().build()), + appBundle, ApkGenerationConfiguration.getDefaultInstance(), lPlusVariantTargeting(), /* allModuleNames= */ ImmutableSet.of(), @@ -252,6 +248,11 @@ private ModuleSplit removeRequiredByPrivacySandboxSdkAttributes(ModuleSplit modu /** Common modifications to both the instant and installed splits. */ private ImmutableList splitModuleInternal() { ImmutableList moduleSplits = runSplitters(); + int baseModuleMinSdk = + apkGenerationConfiguration.getEnableBaseModuleMinSdkAsDefaultTargeting() + && appBundle.hasBaseModule() + ? appBundle.getBaseModule().getAndroidManifest().getEffectiveMinSdkVersion() + : 1; int masterSplitMinSdk = moduleSplits.stream() .filter(ModuleSplit::isMasterSplit) @@ -265,7 +266,9 @@ private ImmutableList splitModuleInternal() { .map(binaryArtProfilesInjector::inject) .map(runtimeEnabledSdkTableInjector::inject) .map(this::addApkTargetingForSigningConfiguration) - .map(moduleSplit -> addDefaultSdkApkTargeting(moduleSplit, masterSplitMinSdk)) + .map( + moduleSplit -> + addDefaultSdkApkTargeting(moduleSplit, masterSplitMinSdk, baseModuleMinSdk)) .map(this::writeSplitIdInManifest) .map(ModuleSplit::addApplicationElementIfMissingInManifest) .collect(toImmutableList()); @@ -505,7 +508,8 @@ private static boolean targetsOnlyPreL(BundleModule module) { * Adds default SDK targeting to the Apk targeting of module split. If SDK targeting already * exists, it's not overridden but checked that it targets no L- devices. */ - private ModuleSplit addDefaultSdkApkTargeting(ModuleSplit split, int masterSplitMinSdk) { + private ModuleSplit addDefaultSdkApkTargeting( + ModuleSplit split, int masterSplitMinSdk, int baseModuleMinSdk) { if (split.getApkTargeting().hasSdkVersionTargeting()) { checkState( split.getApkTargeting().getSdkVersionTargeting().getValue(0).getMin().getValue() @@ -514,7 +518,7 @@ private ModuleSplit addDefaultSdkApkTargeting(ModuleSplit split, int masterSplit return split; } - int defaultSdkVersion = max(masterSplitMinSdk, ANDROID_L_API_VERSION); + int defaultSdkVersion = max(masterSplitMinSdk, baseModuleMinSdk, ANDROID_L_API_VERSION); return split.toBuilder() .setApkTargeting( split.getApkTargeting().toBuilder() diff --git a/src/main/java/com/android/tools/build/bundletool/validation/CountrySetParityValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/CountrySetParityValidator.java index a94f945e..72e20a63 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/CountrySetParityValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/CountrySetParityValidator.java @@ -45,6 +45,7 @@ public class CountrySetParityValidator extends SubValidator { */ @AutoValue public abstract static class SupportedCountrySets { + public static CountrySetParityValidator.SupportedCountrySets create( ImmutableSet countrySets, boolean hasFallback) { return new AutoValue_CountrySetParityValidator_SupportedCountrySets(countrySets, hasFallback); @@ -88,6 +89,7 @@ public void validateAllModules(ImmutableList modules) { continue; } + validateModuleHasFallbackCountrySet(module, moduleCountrySets); if (referenceCountrySets == null) { referenceModule = module; referenceCountrySets = moduleCountrySets; @@ -132,20 +134,9 @@ private void validateDefaultCountrySetSupportedByAllModules( defaultCountrySet, module.getName()) .build(); } - } else { - if (!supportedCountrySets.getHasFallback()) { - throw InvalidBundleException.builder() - .withUserMessage( - "When a standalone or universal APK is built, the fallback country set" - + " folders (folders without #countries suffixes) will be used, but" - + " module: '%s' has no such folders. Either add" - + " missing folders or change the configuration for the COUNTRY_SET" - + " optimization to specify a default suffix corresponding to country" - + " set to use in the standalone and universal APKs.", - module.getName()) - .build(); - } } + + validateModuleHasFallbackCountrySet(module, supportedCountrySets); }); } @@ -179,4 +170,19 @@ private SupportedCountrySets getSupportedCountrySets(BundleModule module) { return SupportedCountrySets.create(countrySets, hasFallback); } + + private static void validateModuleHasFallbackCountrySet( + BundleModule module, SupportedCountrySets supportedCountrySets) { + if (!supportedCountrySets.getHasFallback()) { + throw InvalidBundleException.builder() + .withUserMessage( + "Module '%s' targets content based on country set but doesn't have fallback" + + " folders (folders without #countries suffixes). Fallback folders" + + " will be used to generate split for rest of the countries which are" + + " not targeted by existing country sets. Please add missing folders" + + " and try again.", + module.getName()) + .build(); + } + } } diff --git a/src/main/proto/runtime_enabled_sdk_config.proto b/src/main/proto/runtime_enabled_sdk_config.proto index e275bc0d..54d4f567 100644 --- a/src/main/proto/runtime_enabled_sdk_config.proto +++ b/src/main/proto/runtime_enabled_sdk_config.proto @@ -62,3 +62,16 @@ message CertificateOverride { // Certificate digest that we should override to. string certificate_digest = 2; } + +// Properties that backwards-compatible SDK split inherits from the app. +message SdkSplitPropertiesInheritedFromApp { + // Package name of the app. + string package_name = 1; + // Version code of the app. + int32 version_code = 2; + // minSdkVersion of the app. + int32 min_sdk_version = 3; + // Package ID that the SDK resources should be remapped to so that they do not + // conflict with the app's resources. + int32 resources_package_id = 4; +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommandTest.java new file mode 100644 index 00000000..211f2407 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommandTest.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2023 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.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; +import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; +import static com.android.tools.build.bundletool.testing.Aapt2Helper.AAPT2_PATH; +import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_HOME; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.TestUtils.addKeyToKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.createKeystore; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModules; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForModulesWithoutManifest; +import static com.android.tools.build.bundletool.testing.TestUtils.createZipBuilderForSdkAsarWithModules; +import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredBuilderPropertyException; +import static com.android.tools.build.bundletool.testing.TestUtils.expectMissingRequiredFlagException; +import static com.android.tools.build.bundletool.testing.TestUtils.extractAndroidManifest; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; +import com.android.bundle.RuntimeEnabledSdkConfigProto.SdkSplitPropertiesInheritedFromApp; +import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; +import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; +import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.io.AppBundleSerializer; +import com.android.tools.build.bundletool.io.ZipBuilder; +import com.android.tools.build.bundletool.model.AndroidManifest; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.SigningConfiguration; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; +import com.android.tools.build.bundletool.model.utils.ZipUtils; +import com.android.tools.build.bundletool.testing.ApkSetUtils; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.android.tools.build.bundletool.testing.CertificateFactory; +import com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider; +import com.android.tools.build.bundletool.testing.SdkBundleBuilder; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.hash.HashCode; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteSource; +import com.google.protobuf.util.JsonFormat; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.zip.ZipFile; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BuildSdkApksForAppCommandTest { + + private static final String KEYSTORE_PASSWORD = "keystore-password"; + private static final String KEY_PASSWORD = "key-password"; + private static final String KEY_ALIAS = "key-alias"; + private static final SdkSplitPropertiesInheritedFromApp INHERITED_APP_PROPERTIES = + SdkSplitPropertiesInheritedFromApp.newBuilder() + .setPackageName("com.test.app") + .setMinSdkVersion(26) + .setVersionCode(12345) + .setResourcesPackageId(2) + .build(); + + private final SystemEnvironmentProvider systemEnvironmentProvider = + new FakeSystemEnvironmentProvider(ImmutableMap.of(ANDROID_HOME, "/android/home")); + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + + private Path tmpDir; + private Path sdkAsarPath; + private Path appBundlePath; + private Path extractedModulesFilePath; + private Path inheritedAppPropertiesConfigPath; + private Path outputFilePath; + private Path buildApksOutputFilePath; + + private Path keystorePath; + private static PrivateKey privateKey; + private static X509Certificate certificate; + + @BeforeClass + public static void setUpClass() throws Exception { + // Creating a new key takes in average 75ms (with peaks at 200ms), so creating a single one for + // all the tests. + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(/* keysize= */ 3072); + KeyPair keyPair = kpg.genKeyPair(); + privateKey = keyPair.getPrivate(); + certificate = CertificateFactory.buildSelfSignedCertificate(keyPair, "CN=BuildApksCommandTest"); + } + + @Before + public void setUp() throws Exception { + tmpDir = tmp.getRoot().toPath(); + sdkAsarPath = tmpDir.resolve("sdk.asar"); + appBundlePath = tmpDir.resolve("app.aab"); + inheritedAppPropertiesConfigPath = tmpDir.resolve("config.json"); + Files.writeString( + inheritedAppPropertiesConfigPath, JsonFormat.printer().print(INHERITED_APP_PROPERTIES)); + extractedModulesFilePath = tmpDir.resolve(EXTRACTED_SDK_MODULES_FILE_NAME); + outputFilePath = tmpDir.resolve("output.apks"); + buildApksOutputFilePath = tmpDir.resolve("build-apks-output.apks"); + + // Keystore. + keystorePath = tmpDir.resolve("keystore.jks"); + createKeystore(keystorePath, KEYSTORE_PASSWORD); + addKeyToKeystore( + keystorePath, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD, privateKey, certificate); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_defaults() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BuildSdkApksForAppCommand commandViaFlags = + BuildSdkApksForAppCommand.fromFlags( + new FlagParser() + .parse( + "--sdk-archive=" + sdkAsarPath, + "--app-properties=" + inheritedAppPropertiesConfigPath, + "--output=" + outputFilePath, + "--aapt2=" + AAPT2_PATH), + new PrintStream(output), + systemEnvironmentProvider); + + BuildSdkApksForAppCommand.Builder commandViaBuilder = + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePath) + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setExecutorService(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true); + DebugKeystoreUtils.getDebugSigningConfiguration(systemEnvironmentProvider) + .ifPresent(commandViaBuilder::setSigningConfiguration); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_optionalSigning() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BuildSdkApksForAppCommand commandViaFlags = + BuildSdkApksForAppCommand.fromFlags( + new FlagParser() + .parse( + "--sdk-archive=" + sdkAsarPath, + "--app-properties=" + inheritedAppPropertiesConfigPath, + "--output=" + outputFilePath, + "--aapt2=" + AAPT2_PATH, + // Optional values. + "--ks=" + keystorePath, + "--ks-key-alias=" + KEY_ALIAS, + "--ks-pass=pass:" + KEYSTORE_PASSWORD, + "--key-pass=pass:" + KEY_PASSWORD), + new PrintStream(output), + systemEnvironmentProvider); + + BuildSdkApksForAppCommand.Builder commandViaBuilder = + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePath) + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setExecutorService(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + // Optional values. + .setSigningConfiguration( + SigningConfiguration.builder().setSignerConfig(privateKey, certificate).build()); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + + @Test + public void sdkAsarNotSet_throws() { + expectMissingRequiredBuilderPropertyException( + "sdkArchivePath", + () -> + BuildSdkApksForAppCommand.builder() + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePath) + .build()); + + expectMissingRequiredFlagException( + "sdk-archive", + () -> + BuildSdkApksForAppCommand.fromFlags( + new FlagParser() + .parse( + "--app-properties=" + inheritedAppPropertiesConfigPath, + "--output=" + outputFilePath))); + } + + @Test + public void inheritedAppPropertiesNotSet_throws() { + expectMissingRequiredBuilderPropertyException( + "inheritedAppProperties", + () -> + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setOutputFile(outputFilePath) + .build()); + + expectMissingRequiredFlagException( + "app-properties", + () -> + BuildSdkApksForAppCommand.fromFlags( + new FlagParser() + .parse("--sdk-archive=" + sdkAsarPath, "--output=" + outputFilePath))); + } + + @Test + public void outputFileNotSet_throws() { + expectMissingRequiredBuilderPropertyException( + "outputFile", + () -> + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setInheritedAppProperties(inheritedAppPropertiesConfigPath) + .build()); + + expectMissingRequiredFlagException( + "output", + () -> + BuildSdkApksForAppCommand.fromFlags( + new FlagParser() + .parse( + "--sdk-archive=" + sdkAsarPath, + "--app-properties=" + inheritedAppPropertiesConfigPath))); + } + + @Test + public void modulesZipMissingManifestInAsar_validationFails() throws Exception { + createZipBuilderForSdkAsarWithModules( + createZipBuilderForModulesWithoutManifest(), extractedModulesFilePath) + .writeTo(sdkAsarPath); + BuildSdkApksForAppCommand command = + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePath) + .build(); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains("Module 'base' is missing mandatory file 'manifest/AndroidManifest.xml'."); + } + + @Test + public void generatesModuleSplit() throws Exception { + ZipBuilder asarZipBuilder = + createZipBuilderForSdkAsarWithModules( + createZipBuilderForModules(), extractedModulesFilePath); + asarZipBuilder.writeTo(sdkAsarPath); + BuildSdkApksForAppCommand command = + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePath) + .build(); + + command.execute(); + + ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); + assertThat(apkSetFile.size()).isEqualTo(1); + String apkPathInsideArchive = + "splits/" + SdkBundleBuilder.PACKAGE_NAME.replace(".", "") + "-master.apk"; + assertThat(ZipUtils.allFileEntriesPaths(apkSetFile)) + .containsExactly(ZipPath.create(apkPathInsideArchive)); + File apkFile = ApkSetUtils.extractFromApkSetFile(apkSetFile, apkPathInsideArchive, tmpDir); + AndroidManifest apkManifest = extractAndroidManifest(apkFile, tmpDir); + assertThat(apkManifest.getPackageName()).isEqualTo(INHERITED_APP_PROPERTIES.getPackageName()); + assertThat(apkManifest.getVersionCode()).hasValue(INHERITED_APP_PROPERTIES.getVersionCode()); + assertThat(apkManifest.getMinSdkVersion()) + .hasValue(INHERITED_APP_PROPERTIES.getMinSdkVersion()); + } + + @Test + public void generateModuleSplit_sameAsBuildApks() throws Exception { + String validCertDigest = + "96:C7:EC:89:3E:69:2A:25:BA:4D:EE:C1:84:E8:33:3F:34:7D:6D:12:26:A1:C1:AA:70:A2:8A:DB:75:3E:02:0A"; + ZipBuilder asarZipBuilder = + createZipBuilderForSdkAsarWithModules( + createZipBuilderForModules(), + SdkMetadata.newBuilder() + .setPackageName(SdkBundleBuilder.PACKAGE_NAME) + .setSdkVersion(RuntimeEnabledSdkVersion.newBuilder().setMajor(1).setMinor(1)) + .setCertificateDigest(validCertDigest) + .build(), + extractedModulesFilePath); + asarZipBuilder.writeTo(sdkAsarPath); + BuildSdkApksForAppCommand buildSdkApksForAppCommand = + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePath) + .build(); + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.app", withMinSdkVersion(ANDROID_L_API_VERSION))) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName(SdkBundleBuilder.PACKAGE_NAME) + .setVersionMajor(1) + .setVersionMinor(1) + .setCertificateDigest(validCertDigest) + .setResourcesPackageId(2)) + .build()) + .build()) + .build(); + new AppBundleSerializer().writeToDisk(appBundle, appBundlePath); + BuildApksCommand buildApksCommand = + BuildApksCommand.builder() + .setBundlePath(appBundlePath) + .setOutputFile(buildApksOutputFilePath) + .setRuntimeEnabledSdkArchivePaths(ImmutableSet.of(sdkAsarPath)) + .build(); + + buildSdkApksForAppCommand.execute(); + buildApksCommand.execute(); + + String sdkSplitPath = + "splits/" + SdkBundleBuilder.PACKAGE_NAME.replace(".", "") + "-master.apk"; + ZipFile buildApksOutputSet = new ZipFile(buildApksOutputFilePath.toFile()); + File buildApksOutputApk = + ApkSetUtils.extractFromApkSetFile(buildApksOutputSet, sdkSplitPath, tmpDir); + ZipFile buildSdkApksForApOutputSet = new ZipFile(outputFilePath.toFile()); + File buildSdkApksForAppOutputApk = + ApkSetUtils.extractFromApkSetFile(buildSdkApksForApOutputSet, sdkSplitPath, tmpDir); + assertThat(getFileHash(buildSdkApksForAppOutputApk)).isEqualTo(getFileHash(buildApksOutputApk)); + } + + @Test + public void printHelpDoesNotCrash() { + BuildSdkApksForAppCommand.help(); + } + + private static HashCode getFileHash(File file) throws Exception { + return ByteSource.wrap(Files.newInputStream(file.toPath()).readAllBytes()) + .hash(Hashing.sha256()); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java index 45243e4f..71e05afc 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java @@ -34,6 +34,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.MODULE_TYPE_ASSET_VALUE; import static com.android.tools.build.bundletool.model.AndroidManifest.MODULE_TYPE_FEATURE_VALUE; +import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME; @@ -82,6 +83,7 @@ import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.android.aapt.Resources.XmlElement; import com.android.aapt.Resources.XmlNode; import com.android.tools.build.bundletool.TestData; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; @@ -1469,4 +1471,25 @@ public void getAuthoritiesAttribute_present() { assertThat(androidManifest.getAuthoritiesAttribute()).hasValue("my.package.customAuthority"); } + + @Test + public void getUsesFeatureElement_present() { + XmlElement usesFeatureElement = + xmlElement( + "uses-feature", + xmlAttribute( + ANDROID_NAMESPACE_URI, NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID, "featureName")); + AndroidManifest androidManifest = + AndroidManifest.create(xmlNode(xmlElement("manifest", xmlNode(usesFeatureElement)))); + + assertThat(androidManifest.getUsesFeatureElement("featureName")) + .containsExactlyElementsIn(ImmutableList.of(new XmlProtoElement(usesFeatureElement))); + } + + @Test + public void getUsesFeatureElement_absent() { + AndroidManifest androidManifest = AndroidManifest.create(xmlNode(xmlElement("manifest"))); + + assertThat(androidManifest.getUsesFeatureElement("featureName")).isEmpty(); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java index 7d5a655f..398904f0 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java @@ -1389,6 +1389,38 @@ public void removeRequiredByPrivacySandboxSdkAttributes() { xmlNode(xmlElement(REMOVABLE_ELEMENT_NAME)))))))))); } + @Test + public void addManifestChildElement() { + AndroidManifest androidManifest = AndroidManifest.create(xmlNode(xmlElement("manifest"))); + + AndroidManifest editedManifest = + androidManifest + .toEditor() + .addManifestChildElement( + new XmlProtoElement( + xmlElement( + USES_FEATURE_ELEMENT_NAME, + ImmutableList.of( + xmlAttribute( + ANDROID_NAMESPACE_URI, + NAME_ATTRIBUTE_NAME, + NAME_RESOURCE_ID, + "featureName"))))) + .save(); + + assertThat(editedManifest.getManifestElement().getProto().getChildList()) + .containsExactly( + xmlNode( + xmlElement( + USES_FEATURE_ELEMENT_NAME, + ImmutableList.of( + xmlAttribute( + ANDROID_NAMESPACE_URI, + NAME_ATTRIBUTE_NAME, + NAME_RESOURCE_ID, + "featureName"))))); + } + private static void assertUsesSdkLibraryAttributes( XmlElement usesSdkLibraryElement, String name, long versionMajor, String certDigest) { assertThat(usesSdkLibraryElement.getAttributeList()).hasSize(3); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java index d05284e9..9cbd9a56 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/AssetModuleSplitterTest.java @@ -19,10 +19,14 @@ import static com.android.tools.build.bundletool.model.OptimizationDimension.COUNTRY_SET; import static com.android.tools.build.bundletool.model.OptimizationDimension.LANGUAGE; import static com.android.tools.build.bundletool.model.OptimizationDimension.TEXTURE_COMPRESSION_FORMAT; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifestForAssetModule; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallTimeDelivery; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMinSdkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkTextureTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assets; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; @@ -41,10 +45,12 @@ import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.AssetsDirectoryTargeting; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -60,6 +66,14 @@ public class AssetModuleSplitterTest { private static final String MODULE_NAME = "test_module"; + private static final BundleModule BASE_MODULE = + new BundleModuleBuilder("base") + .addFile("dex/classes.dex") + .setManifest(androidManifest("com.test.app")) + .build(); + + private static final AppBundle APP_BUNDLE = new AppBundleBuilder().addModule(BASE_MODULE).build(); + @Test public void singleSlice() throws Exception { BundleModule testModule = @@ -71,7 +85,8 @@ public void singleSlice() throws Exception { assertThat(testModule.getModuleType()).isEqualTo(ModuleType.ASSET_MODULE); ImmutableList slices = - new AssetModuleSplitter(testModule, ApkGenerationConfiguration.getDefaultInstance()) + new AssetModuleSplitter( + testModule, ApkGenerationConfiguration.getDefaultInstance(), APP_BUNDLE) .splitModule(); assertThat(slices).hasSize(1); @@ -118,7 +133,8 @@ public void slicesByCountrySet() throws Exception { testModule, ApkGenerationConfiguration.builder() .setOptimizationDimensions(ImmutableSet.of(COUNTRY_SET)) - .build()) + .build(), + APP_BUNDLE) .splitModule(); assertThat(slices).hasSize(4); @@ -179,4 +195,70 @@ public void slicesByCountrySet() throws Exception { assertThat(extractPaths(restOfWorldSplit.getEntries())) .containsExactly("assets/images/image.jpg"); } + + @Test + public void singleSlice_updatesMinSdkVersionFromBaseModule_flagEnabled() throws Exception { + BundleModule baseModule = + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(23))) + .build(); + BundleModule testModule = + new BundleModuleBuilder(MODULE_NAME) + .addFile("assets/image.jpg") + .addFile("assets/image2.jpg") + .setManifest(androidManifestForAssetModule("com.test.app", withInstallTimeDelivery())) + .build(); + AppBundle appBundle = + new AppBundleBuilder().addModule(baseModule).addModule(testModule).build(); + + ImmutableList slices = + new AssetModuleSplitter( + testModule, + ApkGenerationConfiguration.builder() + .setEnableBaseModuleMinSdkAsDefaultTargeting(true) + .build(), + appBundle) + .splitModule(); + + assertThat(slices).hasSize(1); + ModuleSplit masterSlice = slices.get(0); + assertThat(masterSlice.getSplitType()).isEqualTo(SplitType.ASSET_SLICE); + assertThat(masterSlice.isMasterSplit()).isTrue(); + assertThat(masterSlice.getAndroidManifest().getSplitId()).hasValue(MODULE_NAME); + assertThat(masterSlice.getAndroidManifest().getHasCode()).hasValue(false); + assertThat(masterSlice.getApkTargeting()).isEqualTo(apkMinSdkTargeting(23)); + assertThat(extractPaths(masterSlice.getEntries())) + .containsExactly("assets/image.jpg", "assets/image2.jpg"); + } + + @Test + public void singleSlice_updatesMinSdkVersionFromBaseModule_flagDisabled() throws Exception { + BundleModule baseModule = + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(23))) + .build(); + BundleModule testModule = + new BundleModuleBuilder(MODULE_NAME) + .addFile("assets/image.jpg") + .addFile("assets/image2.jpg") + .setManifest(androidManifestForAssetModule("com.test.app", withInstallTimeDelivery())) + .build(); + AppBundle appBundle = + new AppBundleBuilder().addModule(baseModule).addModule(testModule).build(); + + ImmutableList slices = + new AssetModuleSplitter( + testModule, ApkGenerationConfiguration.getDefaultInstance(), appBundle) + .splitModule(); + + assertThat(slices).hasSize(1); + ModuleSplit masterSlice = slices.get(0); + assertThat(masterSlice.getSplitType()).isEqualTo(SplitType.ASSET_SLICE); + assertThat(masterSlice.isMasterSplit()).isTrue(); + assertThat(masterSlice.getAndroidManifest().getSplitId()).hasValue(MODULE_NAME); + assertThat(masterSlice.getAndroidManifest().getHasCode()).hasValue(false); + assertThat(masterSlice.getApkTargeting()).isEqualTo(apkMinSdkTargeting(21)); + assertThat(extractPaths(masterSlice.getEntries())) + .containsExactly("assets/image.jpg", "assets/image2.jpg"); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java index 493f0cc2..27adfc19 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ModuleSplitterTest.java @@ -110,6 +110,7 @@ import com.android.aapt.Resources.ResourceTable; import com.android.aapt.Resources.XmlElement; import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Config.BundleConfig; import com.android.bundle.Config.SuffixStripping; import com.android.bundle.Files.Assets; import com.android.bundle.Files.NativeLibraries; @@ -121,6 +122,7 @@ import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleEntry; @@ -164,7 +166,13 @@ public class ModuleSplitterTest { private static final String VALID_CERT_DIGEST = "96:C7:EC:89:3E:69:2A:25:BA:4D:EE:C1:84:E8:33:3F:34:7D:6D:12:26:A1:C1:AA:70:A2:8A:DB:75:3E:02:0A"; - private static final AppBundle APP_BUNDLE = new AppBundleBuilder().build(); + private static final BundleModule BASE_MODULE = + new BundleModuleBuilder("base") + .addFile("dex/classes.dex") + .setManifest(androidManifest("com.test.app")) + .build(); + + private static final AppBundle APP_BUNDLE = new AppBundleBuilder().addModule(BASE_MODULE).build(); @Test public void minSdkVersionInOutputTargeting_getsSetToL() throws Exception { @@ -172,7 +180,14 @@ public void minSdkVersionInOutputTargeting_getsSetToL() throws Exception { new BundleModuleBuilder("testModule").setManifest(androidManifest("com.test.app")).build(); ImmutableList moduleSplits = - ModuleSplitter.createForTest(bundleModule, BUNDLETOOL_VERSION).splitModule(); + ModuleSplitter.createForTest( + bundleModule, + AppBundle.buildFromModules( + ImmutableList.of(bundleModule, BASE_MODULE), + BundleConfig.getDefaultInstance(), + BundleMetadata.builder().build()), + BUNDLETOOL_VERSION) + .splitModule(); assertThat(moduleSplits).hasSize(1); ModuleSplit masterSplit = moduleSplits.get(0); @@ -197,7 +212,7 @@ public void rPlusSigningConfigWithRPlusVariant_minSdkVersionInOutputTargetingGet .setMinSdkForAdditionalVariantWithV3Rotation(ANDROID_R_API_VERSION) .build(), variantMinSdkTargeting(Versions.ANDROID_R_API_VERSION), - ImmutableSet.of("testModule")) + ImmutableSet.of("base", "testModule")) .splitModule(); assertThat(moduleSplits.stream().map(ModuleSplit::getApkTargeting).distinct()) @@ -219,7 +234,7 @@ public void rPlusSigningConfigWithDefaultVariant_minSdkVersionInOutputTargetingN .setMinSdkForAdditionalVariantWithV3Rotation(ANDROID_R_API_VERSION) .build(), VariantTargeting.getDefaultInstance(), - ImmutableSet.of("testModule")) + ImmutableSet.of("base", "testModule")) .splitModule(); assertThat(moduleSplits.stream().map(ModuleSplit::getApkTargeting)) @@ -239,7 +254,7 @@ public void defaultSigningConfigWithRPlusVariant_minSdkVersionInOutputTargetingN APP_BUNDLE, ApkGenerationConfiguration.getDefaultInstance(), variantMinSdkTargeting(Versions.ANDROID_R_API_VERSION), - ImmutableSet.of("testModule")) + ImmutableSet.of("base", "testModule")) .splitModule(); assertThat(moduleSplits.stream().map(ModuleSplit::getApkTargeting)) @@ -839,7 +854,7 @@ public void nativeSplits_lPlusTargeting_withAbiAndUncompressNativeLibsSplitter() .setEnableUncompressedNativeLibraries(true) .build(), lPlusVariantTargeting(), - ImmutableSet.of("testModule")); + ImmutableSet.of("base", "testModule")); List splits = moduleSplitter.splitModule(); // Base + X86 Split @@ -878,7 +893,7 @@ public void nativeSplits_mPlusTargeting_withAbiAndUncompressNativeLibsSplitter() .setEnableUncompressedNativeLibraries(true) .build(), variantMinSdkTargeting(ANDROID_M_API_VERSION), - ImmutableSet.of("testModule")); + ImmutableSet.of("base", "testModule")); List splits = moduleSplitter.splitModule(); // Base + X86 Split @@ -916,7 +931,7 @@ public void nativeSplits_mPlusTargeting_disabledUncompressedSplitter() { .setEnableUncompressedNativeLibraries(false) .build(), variantMinSdkTargeting(ANDROID_M_API_VERSION), - ImmutableSet.of("testModule")); + ImmutableSet.of("base", "testModule")); List splits = moduleSplitter.splitModule(); assertThat(splits).hasSize(2); @@ -1089,7 +1104,7 @@ public void nativeSplits_pPlusTargeting_withDexCompressionSplitter() throws Exce APP_BUNDLE, ApkGenerationConfiguration.builder().setEnableDexCompressionSplitter(true).build(), variantMinSdkTargeting(ANDROID_Q_API_VERSION), - ImmutableSet.of("testModule")); + ImmutableSet.of("base", "testModule")); List splits = moduleSplitter.splitModule(); assertThat(splits).hasSize(1); @@ -1112,7 +1127,7 @@ public void nativeSplits_withSparseEncodingSplitter_withSdk32Variant() throws Ex APP_BUNDLE, ApkGenerationConfiguration.builder().setEnableSparseEncodingVariant(true).build(), variantMinSdkTargeting(ANDROID_S_V2_API_VERSION), - ImmutableSet.of("testModule")); + ImmutableSet.of("base", "testModule")); ImmutableList splits = moduleSplitter.splitModule(); assertThat(splits).hasSize(1); @@ -1134,7 +1149,7 @@ public void nativeSplits_withSparseEncodingSplitter_withSdk21Variant() throws Ex APP_BUNDLE, ApkGenerationConfiguration.builder().setEnableSparseEncodingVariant(true).build(), variantMinSdkTargeting(ANDROID_L_API_VERSION), - ImmutableSet.of("testModule")); + ImmutableSet.of("base", "testModule")); ImmutableList splits = moduleSplitter.splitModule(); assertThat(splits).hasSize(1); @@ -1159,7 +1174,7 @@ public void nativeSplits_lPlusTargeting_withDexCompressionSplitter() throws Exce APP_BUNDLE, ApkGenerationConfiguration.builder().setEnableDexCompressionSplitter(true).build(), lPlusVariantTargeting(), - ImmutableSet.of("testModule")); + ImmutableSet.of("base", "testModule")); List splits = moduleSplitter.splitModule(); assertThat(splits).hasSize(1); @@ -1443,7 +1458,15 @@ public void targetsPreLOnlyInManifest_throws() throws Exception { CommandExecutionException exception = assertThrows( CommandExecutionException.class, - () -> ModuleSplitter.createForTest(bundleModule, BUNDLETOOL_VERSION).splitModule()); + () -> + ModuleSplitter.createForTest( + bundleModule, + AppBundle.buildFromModules( + ImmutableList.of(bundleModule, BASE_MODULE), + BundleConfig.getDefaultInstance(), + BundleMetadata.builder().build()), + BUNDLETOOL_VERSION) + .splitModule()); assertThat(exception) .hasMessageThat() @@ -1480,7 +1503,14 @@ public void resolvesSplitIdSuffixes_singleVariant() throws Exception { BundleModule bundleModule = new BundleModuleBuilder("base").setManifest(androidManifest("com.test.app")).build(); - ModuleSplitter moduleSplitter = ModuleSplitter.createForTest(bundleModule, BUNDLETOOL_VERSION); + ModuleSplitter moduleSplitter = + ModuleSplitter.createForTest( + bundleModule, + AppBundle.buildFromModules( + ImmutableList.of(bundleModule), + BundleConfig.getDefaultInstance(), + BundleMetadata.builder().build()), + BUNDLETOOL_VERSION); assetsSplit1 = moduleSplitter.writeSplitIdInManifest(assetsSplit1); assetsSplit2 = moduleSplitter.writeSplitIdInManifest(assetsSplit2); @@ -1518,7 +1548,14 @@ public void resolvesSplitIdSuffixes_multipleVariants() throws Exception { BundleModule bundleModule = new BundleModuleBuilder("base").setManifest(androidManifest("com.test.app")).build(); - ModuleSplitter moduleSplitter = ModuleSplitter.createForTest(bundleModule, BUNDLETOOL_VERSION); + ModuleSplitter moduleSplitter = + ModuleSplitter.createForTest( + bundleModule, + AppBundle.buildFromModules( + ImmutableList.of(bundleModule), + BundleConfig.getDefaultInstance(), + BundleMetadata.builder().build()), + BUNDLETOOL_VERSION); assetsSplit1 = moduleSplitter.writeSplitIdInManifest(assetsSplit1); assetsSplit2 = moduleSplitter.writeSplitIdInManifest(assetsSplit2); @@ -1536,7 +1573,14 @@ public void splitNameRemovedForInstalledSplit() throws Exception { BundleModule bundleModule = new BundleModuleBuilder("testModule").setManifest(manifest).build(); ImmutableList moduleSplits = - ModuleSplitter.createForTest(bundleModule, BUNDLETOOL_VERSION).splitModule(); + ModuleSplitter.createForTest( + bundleModule, + AppBundle.buildFromModules( + ImmutableList.of(bundleModule, BASE_MODULE), + BundleConfig.getDefaultInstance(), + BundleMetadata.builder().build()), + BUNDLETOOL_VERSION) + .splitModule(); assertThat(moduleSplits).hasSize(1); ModuleSplit masterSplit = moduleSplits.get(0); @@ -1572,7 +1616,7 @@ public void splitNameNotRemovedForInstantSplit() throws Exception { APP_BUNDLE, ApkGenerationConfiguration.builder().setForInstantAppVariants(true).build(), lPlusVariantTargeting(), - ImmutableSet.of("testModule")) + ImmutableSet.of("base", "testModule")) .splitModule(); assertThat(moduleSplits).hasSize(1); @@ -1603,7 +1647,14 @@ public void applicationElementAdded() throws Exception { BundleModule bundleModule = new BundleModuleBuilder("testModule").setManifest(manifest).build(); ImmutableList moduleSplits = - ModuleSplitter.createForTest(bundleModule, BUNDLETOOL_VERSION).splitModule(); + ModuleSplitter.createForTest( + bundleModule, + AppBundle.buildFromModules( + ImmutableList.of(bundleModule, BASE_MODULE), + BundleConfig.getDefaultInstance(), + BundleMetadata.builder().build()), + BUNDLETOOL_VERSION) + .splitModule(); assertThat(moduleSplits).hasSize(1); ModuleSplit masterSplit = moduleSplits.get(0); @@ -1633,7 +1684,7 @@ public void nonInstantActivityRemovedForInstantManifest() throws Exception { APP_BUNDLE, ApkGenerationConfiguration.builder().setForInstantAppVariants(true).build(), lPlusVariantTargeting(), - ImmutableSet.of()) + ImmutableSet.of("base")) .splitModule(); assertThat(moduleSplits).hasSize(1); @@ -1668,7 +1719,7 @@ public void instantManifestChanges_addsMinSdkVersion() throws Exception { APP_BUNDLE, ApkGenerationConfiguration.builder().setForInstantAppVariants(true).build(), lPlusVariantTargeting(), - ImmutableSet.of("testModule")) + ImmutableSet.of("base", "testModule")) .splitModule(); assertThat(moduleSplits).hasSize(1); @@ -1697,7 +1748,7 @@ public void instantManifestChanges_keepsMinSdkVersion() throws Exception { APP_BUNDLE, ApkGenerationConfiguration.builder().setForInstantAppVariants(true).build(), lPlusVariantTargeting(), - ImmutableSet.of("testModule")) + ImmutableSet.of("base", "testModule")) .splitModule(); assertThat(moduleSplits).hasSize(1); @@ -1724,7 +1775,79 @@ public void instantManifestChanges_updatesMinSdkVersion() throws Exception { APP_BUNDLE, ApkGenerationConfiguration.builder().setForInstantAppVariants(true).build(), lPlusVariantTargeting(), - ImmutableSet.of("testModule")) + ImmutableSet.of("base", "testModule")) + .splitModule(); + + assertThat(moduleSplits).hasSize(1); + ModuleSplit masterSplit = moduleSplits.get(0); + assertThat(masterSplit.getVariantTargeting()).isEqualTo(lPlusVariantTargeting()); + assertThat(masterSplit.isMasterSplit()).isTrue(); + assertThat(masterSplit.getApkTargeting()).isEqualTo(apkMinSdkTargeting(21)); + assertThat(masterSplit.getSplitType()).isEqualTo(SplitType.INSTANT); + assertThat(masterSplit.getAndroidManifest().getTargetSandboxVersion()).hasValue(2); + assertThat(masterSplit.getAndroidManifest().getMinSdkVersion()).hasValue(21); + } + + @Test + public void instantManifestChanges_updatesMinSdkVersionFromBaseModule_flagEnabled() + throws Exception { + BundleModule bundleModule = + new BundleModuleBuilder("testModule") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(19), withInstant(true))) + .build(); + BundleModule baseModule = + new BundleModuleBuilder("base") + .addFile("dex/classes.dex") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(23), withInstant(true))) + .build(); + AppBundle appBundle = + new AppBundleBuilder().addModule(baseModule).addModule(bundleModule).build(); + + ImmutableList moduleSplits = + ModuleSplitter.createNoStamp( + bundleModule, + BUNDLETOOL_VERSION, + appBundle, + ApkGenerationConfiguration.builder() + .setForInstantAppVariants(true) + .setEnableBaseModuleMinSdkAsDefaultTargeting(true) + .build(), + lPlusVariantTargeting(), + ImmutableSet.of("base", "testModule")) + .splitModule(); + + assertThat(moduleSplits).hasSize(1); + ModuleSplit masterSplit = moduleSplits.get(0); + assertThat(masterSplit.getVariantTargeting()).isEqualTo(lPlusVariantTargeting()); + assertThat(masterSplit.isMasterSplit()).isTrue(); + assertThat(masterSplit.getApkTargeting()).isEqualTo(apkMinSdkTargeting(23)); + assertThat(masterSplit.getSplitType()).isEqualTo(SplitType.INSTANT); + assertThat(masterSplit.getAndroidManifest().getTargetSandboxVersion()).hasValue(2); + assertThat(masterSplit.getAndroidManifest().getMinSdkVersion()).hasValue(21); + } + + @Test + public void instantManifestChanges_keepsMinSdkVersion_flagDisabled() throws Exception { + BundleModule bundleModule = + new BundleModuleBuilder("testModule") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(19), withInstant(true))) + .build(); + BundleModule baseModule = + new BundleModuleBuilder("base") + .addFile("dex/classes.dex") + .setManifest(androidManifest("com.test.app", withMinSdkVersion(23), withInstant(true))) + .build(); + AppBundle appBundle = + new AppBundleBuilder().addModule(baseModule).addModule(bundleModule).build(); + + ImmutableList moduleSplits = + ModuleSplitter.createNoStamp( + bundleModule, + BUNDLETOOL_VERSION, + appBundle, + ApkGenerationConfiguration.builder().setForInstantAppVariants(true).build(), + lPlusVariantTargeting(), + ImmutableSet.of("base", "testModule")) .splitModule(); assertThat(moduleSplits).hasSize(1); @@ -1835,7 +1958,7 @@ public void addingLibraryPlaceholders_featureModule_noAction() throws Exception ImmutableSet.of(toAbi(AbiAlias.X86), toAbi(AbiAlias.ARM64_V8A))) .build(), lPlusVariantTargeting(), - ImmutableSet.of("feature")); + ImmutableSet.of("base", "feature")); ImmutableList splits = moduleSplitter.splitModule(); assertThat(splits).hasSize(1); @@ -2124,7 +2247,7 @@ public void testModuleSplitter_nativeSplit_addsNoStamp() throws Exception { .setEnableUncompressedNativeLibraries(true) .build(), lPlusVariantTargeting(), - ImmutableSet.of("testModule"), + ImmutableSet.of("base", "testModule"), Optional.of(stampSource), stampType); @@ -2178,6 +2301,7 @@ public void binaryArtProfileIsCopied() { .build(); AppBundle appBundle = new AppBundleBuilder() + .addModule(BASE_MODULE) .addMetadataFile( BinaryArtProfilesInjector.METADATA_NAMESPACE, BinaryArtProfilesInjector.BINARY_ART_PROFILE_NAME, @@ -2258,7 +2382,8 @@ public void bundleHasRuntimeEnabledSdkDeps_sdkRuntimeVariant_featureModule_noUse .setManifest(androidManifest("com.test.app")) .setRuntimeEnabledSdkConfig(runtimeEnabledSdkConfig) .build(); - AppBundle appBundle = new AppBundleBuilder().addModule(testModule).build(); + AppBundle appBundle = + new AppBundleBuilder().addModule(BASE_MODULE).addModule(testModule).build(); ModuleSplitter moduleSplitter = ModuleSplitter.createNoStamp( testModule, @@ -2266,7 +2391,7 @@ public void bundleHasRuntimeEnabledSdkDeps_sdkRuntimeVariant_featureModule_noUse appBundle, ApkGenerationConfiguration.getDefaultInstance(), sdkRuntimeVariantTargeting(), - ImmutableSet.of("feature")); + ImmutableSet.of("base", "feature")); ImmutableList splits = moduleSplitter.splitModule(); diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java index f6352839..1bbe313b 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java @@ -71,6 +71,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.TOOLS_NAMESPACE_URI; +import static com.android.tools.build.bundletool.model.AndroidManifest.USES_FEATURE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_SDK_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_SDK_LIBRARY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.VALUE_ATTRIBUTE_NAME; @@ -1106,6 +1107,21 @@ public static boolean compareManifestMutators( .equals(defaultManifest.applyMutators(ImmutableList.of(otherManifestMutator))); } + /** Adds an element. */ + public static ManifestMutator withUsesFeatureElement(String usesFeatureNameValue) { + return manifestElement -> + manifestElement.addChildElement( + new XmlProtoElementBuilder( + xmlElement( + USES_FEATURE_ELEMENT_NAME, + xmlAttribute( + ANDROID_NAMESPACE_URI, + NAME_ATTRIBUTE_NAME, + NAME_RESOURCE_ID, + usesFeatureNameValue)) + .toBuilder())); + } + // Do not instantiate. private ManifestProtoUtils() {} } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java index c8296f0a..8db8db14 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java @@ -210,11 +210,14 @@ public static ZipBuilder createZipBuilderForSdkAsarWithModules( } public static ZipBuilder createZipBuilderForSdkBundle() { + return createZipBuilderForSdkBundle(SdkBundleConfig.getDefaultInstance()); + } + + public static ZipBuilder createZipBuilderForSdkBundle(SdkBundleConfig sdkBundleConfig) { return new ZipBuilder() .addFileWithContent( ZipPath.create("BUNDLE-METADATA/some.namespace/metadata1"), new byte[] {0x01}) - .addFileWithProtoContent( - ZipPath.create(SDK_BUNDLE_CONFIG_FILE_NAME), SdkBundleConfig.getDefaultInstance()) + .addFileWithProtoContent(ZipPath.create(SDK_BUNDLE_CONFIG_FILE_NAME), sdkBundleConfig) .addFileWithContent(ZipPath.create(SDK_INTERFACE_DESCRIPTORS_FILE_NAME), TEST_CONTENT); } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java index 23b2d64c..efa20d60 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java @@ -59,12 +59,14 @@ public void validateAllModules_sameCountrySets_ok() { new BundleModuleBuilder("a") .addFile("assets/img#countries_latam/image.jpg") .addFile("assets/img#countries_sea/image.jpg") + .addFile("assets/img/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); BundleModule moduleB = new BundleModuleBuilder("b") .addFile("assets/img#countries_latam/image.jpg") .addFile("assets/img#countries_sea/image.jpg") + .addFile("assets/img/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); @@ -79,6 +81,7 @@ public void validateAllModules_multipleFilesPerCountrySetDirectory_ok() { .addFile("assets/img#countries_latam/imageB.jpg") .addFile("assets/img#countries_sea/image1.jpg") .addFile("assets/img#countries_sea/image2.jpg") + .addFile("assets/img/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); @@ -91,12 +94,14 @@ public void validateAllModules_sameCountrySetsAndNoCountrySet_ok() { new BundleModuleBuilder("a") .addFile("assets/img#countries_latam/image.jpg") .addFile("assets/img#countries_sea/image.jpg") + .addFile("assets/img/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); BundleModule moduleB = new BundleModuleBuilder("b") .addFile("assets/img#countries_latam/image.jpg") .addFile("assets/img#countries_sea/image.jpg") + .addFile("assets/img/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); BundleModule moduleC = @@ -110,12 +115,14 @@ public void validateAllModules_differentCountrySetsInDifferentModules_throws() { BundleModule moduleA = new BundleModuleBuilder("a") .addFile("assets/img#countries_latam/image.jpg") + .addFile("assets/img/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); BundleModule moduleB = new BundleModuleBuilder("b") .addFile("assets/img#countries_latam/image.jpg") .addFile("assets/img#countries_sea/image.jpg") + .addFile("assets/img/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); @@ -130,21 +137,14 @@ public void validateAllModules_differentCountrySetsInDifferentModules_throws() { .hasMessageThat() .contains( "All modules with country set targeting must support the same country sets, but module" - + " 'a' supports [latam] (without fallback directories) and module 'b' supports" - + " [latam, sea] (without fallback directories)."); + + " 'a' supports [latam] (with fallback directories) and module 'b' supports" + + " [latam, sea] (with fallback directories)."); } @Test - public void sameCountrySetsInDifferentModules_differentFallback_throws() { + public void validateAllModules_noFallback_throws() { BundleModule moduleA = new BundleModuleBuilder("a") - .addFile("assets/img#countries_latam/image.jpg") - .addFile("assets/img#countries_sea/image.jpg") - .addFile("assets/img/image.jpg") - .setManifest(androidManifest("com.test.app")) - .build(); - BundleModule moduleB = - new BundleModuleBuilder("b") .addFile("assets/img#countries_latam/image.jpg") .addFile("assets/img#countries_sea/image.jpg") .setManifest(androidManifest("com.test.app")) @@ -153,16 +153,15 @@ public void sameCountrySetsInDifferentModules_differentFallback_throws() { InvalidBundleException exception = assertThrows( InvalidBundleException.class, - () -> - new CountrySetParityValidator() - .validateAllModules(ImmutableList.of(moduleA, moduleB))); + () -> new CountrySetParityValidator().validateAllModules(ImmutableList.of(moduleA))); assertThat(exception) .hasMessageThat() .contains( - "All modules with country set targeting must support the same country sets, but module" - + " 'a' supports [latam, sea] (with fallback directories) and module 'b' supports" - + " [latam, sea] (without fallback directories)."); + "Module 'a' targets content based on country set but doesn't have fallback folders" + + " (folders without #countries suffixes). Fallback folders will be used to" + + " generate split for rest of the countries which are not targeted by existing" + + " country sets. Please add missing folders and try again."); } @Test @@ -225,21 +224,22 @@ public void validateBundle_moduleWithoutFallbackCountrySet_throws() { assertThat(exception) .hasMessageThat() .contains( - "When a standalone or universal APK is built, the fallback country set folders (folders" - + " without #countries suffixes) will be used, but module: 'a' has no such folders." - + " Either add missing folders or change the configuration for the COUNTRY_SET" - + " optimization to specify a default suffix corresponding to country set to use in" - + " the standalone and universal APKs."); + "Module 'a' targets content based on country set but doesn't have fallback folders" + + " (folders without #countries suffixes). Fallback folders will be used to" + + " generate split for rest of the countries which are not targeted by existing" + + " country sets. Please add missing folders and try again."); } @Test - public void validateBundle_moduleWithDefaultCountrySet_succeeds() { + public void validateBundle_moduleWithDefaultCountrySetFolders_succeeds() { BundleModule moduleA = new BundleModuleBuilder("a") .addFile("assets/img1#countries_latam/image.jpg") .addFile("assets/img1#countries_sea/image.jpg") + .addFile("assets/img1/image.jpg") .addFile("assets/img2#countries_latam/image.jpg") .addFile("assets/img2#countries_sea/image.jpg") + .addFile("assets/img2/image.jpg") .setManifest(androidManifest("com.test.app")) .build(); AppBundle appBundle = @@ -262,7 +262,7 @@ public void validateBundle_moduleWithDefaultCountrySet_succeeds() { } @Test - public void validateBundle_moduleWithoutDefaultCountrySet_throws() { + public void validateBundle_moduleWithoutDefaultCountrySetFolders_throws() { BundleModule moduleA = new BundleModuleBuilder("a") .addFile("assets/img1#countries_latam/image.jpg")