diff --git a/README.md b/README.md index d67166fd..fce6e5e2 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.13.0](https://github.com/google/bundletool/releases) +Latest release: [1.13.1](https://github.com/google/bundletool/releases) diff --git a/dex_src/README.md b/dex_src/README.md new file mode 100644 index 00000000..27aaba46 --- /dev/null +++ b/dex_src/README.md @@ -0,0 +1 @@ +Contains the source-code for any .dex files in bundletool. diff --git a/gradle.properties b/gradle.properties index 7e2e06ac..f9611cce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.13.0 +release_version = 1.13.1 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 2c428e71..a8e16702 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 @@ -176,6 +176,7 @@ public enum OutputFormat { private static final Flag> SYSTEM_APK_OPTIONS = Flag.enumSet("system-apk-options", SystemApkOption.class); private static final Flag DEVICE_TIER_FLAG = Flag.nonNegativeInteger("device-tier"); + private static final Flag COUNTRY_SET_FLAG = Flag.string("country-set"); private static final Flag VERBOSE_FLAG = Flag.booleanFlag("verbose"); @@ -238,6 +239,8 @@ public enum OutputFormat { public abstract Optional getDeviceTier(); + public abstract Optional getCountrySet(); + public abstract ImmutableSet getSystemApkOptions(); public abstract boolean getGenerateOnlyForConnectedDevice(); @@ -389,6 +392,12 @@ public Builder setDeviceSpec(Path deviceSpecFile) { */ public abstract Builder setDeviceTier(Integer deviceTier); + /** + * Sets the country set to use for APK matching. This will override the country set of the given + * device spec. + */ + public abstract Builder setCountrySet(String countrySet); + /** Sets options to generated APKs in system mode. */ public abstract Builder setSystemApkOptions(ImmutableSet options); @@ -652,6 +661,16 @@ public BuildApksCommand build() { .build(); } + if (command.getCountrySet().isPresent() + && !command.getGenerateOnlyForConnectedDevice() + && !command.getDeviceSpec().isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Setting --country-set requires using either the --connected-device or the" + + " --device-spec flag.") + .build(); + } + if (command.getOutputFormat().equals(APK_SET)) { if (!APK_SET_ARCHIVE_EXTENSION.equals( MoreFiles.getFileExtension(command.getOutputFile()))) { @@ -776,6 +795,7 @@ static BuildApksCommand fromFlags( .map(deviceSpecParser) .ifPresent(buildApksCommand::setDeviceSpec); DEVICE_TIER_FLAG.getValue(flags).ifPresent(buildApksCommand::setDeviceTier); + COUNTRY_SET_FLAG.getValue(flags).ifPresent(buildApksCommand::setCountrySet); MODULES_FLAG.getValue(flags).ifPresent(buildApksCommand::setModules); VERBOSE_FLAG.getValue(flags).ifPresent(buildApksCommand::setVerbose); P7ZIP_PATH_FLAG @@ -1356,7 +1376,7 @@ public static CommandHelp help() { .setDescription( "If set, will generate APK Set optimized for the connected device. The " + "generated APK Set will only be installable on that specific class of " - + "devices. This flag should be only be set with --%s=%s flag.", + + "devices. This flag should only be set with --%s=%s flag.", BUILD_MODE_FLAG.getName(), DEFAULT.getLowerCaseName()) .build()) .addFlag( @@ -1399,12 +1419,24 @@ public static CommandHelp help() { .setExampleValue("low") .setOptional(true) .setDescription( - "Device tier to use for APK matching. This flag should be only be set with" + "Device tier to use for APK matching. This flag should only be set with" + " --%s or --%s flags. If a device spec with a device tier is provided," + " the value specified here will override the value set in the device" + " spec.", DEVICE_SPEC_FLAG.getName(), CONNECTED_DEVICE_FLAG.getName()) .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(COUNTRY_SET_FLAG.getName()) + .setExampleValue("country_set_name") + .setOptional(true) + .setDescription( + "Country set to use for APK matching. This flag should only be set with" + + " --%s or --%s flags. If a device spec with a country set is provided," + + " the value specified here will override the value set in the device" + + " spec.", + DEVICE_SPEC_FLAG.getName(), CONNECTED_DEVICE_FLAG.getName()) + .build()) .addFlag( FlagDescription.builder() .setFlagName(FUSE_ONLY_DEVICE_MATCHING_MODULES_FLAG.getName()) 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 350d562e..610715cc 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 @@ -35,6 +35,7 @@ import com.android.tools.build.bundletool.optimizations.OptimizationsMerger; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import dagger.Module; import dagger.Provides; import java.io.PrintStream; @@ -153,6 +154,15 @@ static Optional provideDeviceSpec(BuildApksCommand command) { .setDeviceTier(Int32Value.of(command.getDeviceTier().get())) .build()); } + if (command.getCountrySet().isPresent()) { + checkState(deviceSpec.isPresent(), "Country set specified but no device was provided"); + deviceSpec = + deviceSpec.map( + spec -> + spec.toBuilder() + .setCountrySet(StringValue.of(command.getCountrySet().get())) + .build()); + } return deviceSpec; } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommand.java index 9b63dc85..26313d1d 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommand.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.commands; import static com.android.tools.build.bundletool.commands.CommandUtils.ANDROID_SERIAL_VARIABLE; +import static com.android.tools.build.bundletool.device.DeviceTargetingConfigEvaluator.getMatchingCountrySet; import static com.android.tools.build.bundletool.device.DeviceTargetingConfigEvaluator.getMatchingDeviceGroups; import static com.android.tools.build.bundletool.device.DeviceTargetingConfigEvaluator.getSelectedDeviceTier; import static com.android.tools.build.bundletool.model.utils.SdkToolsLocator.ANDROID_HOME_VARIABLE; @@ -74,6 +75,8 @@ public abstract class EvaluateDeviceTargetingConfigCommand { private static final Flag DEVICE_ID_FLAG = Flag.string("device-id"); + private static final Flag COUNTRY_CODE_FLAG = Flag.string("country-code"); + private static final SystemEnvironmentProvider DEFAULT_PROVIDER = new DefaultSystemEnvironmentProvider(); @@ -96,6 +99,8 @@ public abstract class EvaluateDeviceTargetingConfigCommand { public abstract Optional getDeviceId(); + public abstract Optional getCountryCode(); + abstract Optional getAdbPath(); static Builder builder() { @@ -114,6 +119,8 @@ abstract static class Builder { abstract Builder setDeviceId(Optional id); + abstract Builder setCountryCode(String countryCode); + abstract EvaluateDeviceTargetingConfigCommand build(); abstract Builder setAdbPath(Path adbPath); @@ -177,6 +184,9 @@ public static EvaluateDeviceTargetingConfigCommand fromFlags( .setConnectedDeviceMode(true) .setDeviceId(DEVICE_ID_FLAG.getValue(flags)); } + COUNTRY_CODE_FLAG + .getValue(flags) + .ifPresent(evaluateDeviceTargetingConfigCommandBuilder::setCountryCode); return evaluateDeviceTargetingConfigCommandBuilder.build(); } @@ -188,6 +198,9 @@ public void execute(PrintStream out) throws IOException, TimeoutException { DeviceTierConfig config = configBuilder.build(); DeviceTierConfigValidator.validateDeviceTierConfig(config); + if (getCountryCode().isPresent()) { + DeviceTierConfigValidator.validateCountryCode(getCountryCode().get()); + } DeviceProperties.Builder devicePropertiesBuilder = DeviceProperties.newBuilder(); if (this.getDevicePropertiesPath().isPresent()) { @@ -202,6 +215,9 @@ public void execute(PrintStream out) throws IOException, TimeoutException { printTier(getSelectedDeviceTier(config, deviceProperties), out); printGroups(getMatchingDeviceGroups(config, deviceProperties), out); + if (getCountryCode().isPresent()) { + printCountrySet(getMatchingCountrySet(config, getCountryCode().get()), out); + } } } @@ -248,6 +264,10 @@ private void printGroups(ImmutableSet deviceGroups, PrintStream out } } + private void printCountrySet(String countrySet, PrintStream out) { + out.println("Country Set: '" + countrySet + "'"); + } + public static CommandHelp help() { return CommandHelp.builder() .setCommandName(COMMAND_NAME) @@ -300,6 +320,16 @@ public static CommandHelp help() { + "device or emulator is connected. Used only if %s flag is set.", ANDROID_SERIAL_VARIABLE, CONNECTED_DEVICE_FLAG) .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(COUNTRY_CODE_FLAG.getName()) + .setExampleValue("VN") + .setOptional(true) + .setDescription( + "An ISO 3166 alpha-2 format country code for the country of user account on the" + + " device. This will be used to derive corresponding country set from" + + " device targeting configuration.") + .build()) .build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java index ce378a52..b3e0f143 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java @@ -54,6 +54,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteStreams; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.io.InputStream; @@ -94,6 +95,8 @@ public abstract class ExtractApksCommand { public abstract Optional> getModules(); + public abstract boolean getIncludeInstallTimeAssetModules(); + /** Gets whether instant APKs should be extracted. */ public abstract boolean getInstant(); @@ -103,7 +106,8 @@ public abstract class ExtractApksCommand { public static Builder builder() { return new AutoValue_ExtractApksCommand.Builder() .setInstant(false) - .setIncludeMetadata(false); + .setIncludeMetadata(false) + .setIncludeInstallTimeAssetModules(true); } /** Builder for the {@link ExtractApksCommand}. */ @@ -119,8 +123,20 @@ public Builder setDeviceSpec(Path deviceSpecPath) { public abstract Builder setOutputDirectory(Path outputDirectory); + /** + * Sets the required modules to extract. + * + *

All install-time feature modules and asset modules are extracted by default. You can + * exclude install-time asset modules by passing {@code false} to {@link + * #setIncludeInstallTimeAssetModules}. + * + *

"_ALL_" extracts all modules. + */ public abstract Builder setModules(ImmutableSet modules); + /** Whether to extract install-time asset modules (default = true). */ + public abstract Builder setIncludeInstallTimeAssetModules(boolean shouldInclude); + /** * Sets whether instant APKs should be extracted. * @@ -195,6 +211,7 @@ ImmutableList execute(PrintStream output) { new ApkMatcher( deviceSpec, requestedModuleNames, + getIncludeInstallTimeAssetModules(), getInstant(), /* ensureDensityAndAbiApksMatched= */ true); ImmutableList generatedApks = apkMatcher.getMatchingApks(toc); @@ -350,6 +367,18 @@ private static DeviceSpec applyDefaultsToDeviceSpec(DeviceSpec deviceSpec, Build .orElse(0); builder.setDeviceTier(Int32Value.of(defaultDeviceTier)); } + if (!deviceSpec.hasCountrySet()) { + String defaultCountrySet = + toc.getDefaultTargetingValueList().stream() + .filter( + defaultTargetingValue -> + defaultTargetingValue.getDimension().equals(Value.COUNTRY_SET)) + .map(DefaultTargetingValue::getDefaultValue) + .filter(defaultValue -> !defaultValue.isEmpty()) + .collect(toOptional()) + .orElse(""); + builder.setCountrySet(StringValue.of(defaultCountrySet)); + } if (!deviceSpec.hasSdkRuntime()) { builder .getSdkRuntimeBuilder() diff --git a/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java index bc517e3c..5f7b7ecf 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommand.java @@ -36,6 +36,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.io.MoreFiles; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.io.UncheckedIOException; @@ -59,6 +60,7 @@ public abstract class GetDeviceSpecCommand { private static final Flag DEVICE_TIER_FLAG = Flag.nonNegativeInteger("device-tier"); private static final Flag> DEVICE_GROUPS_FLAG = Flag.stringSet("device-groups"); + private static final Flag COUNTRY_SET_FLAG = Flag.string("country-set"); private static final SystemEnvironmentProvider DEFAULT_PROVIDER = new DefaultSystemEnvironmentProvider(); @@ -79,6 +81,8 @@ public abstract class GetDeviceSpecCommand { public abstract Optional> getDeviceGroups(); + public abstract Optional getCountrySet(); + public static Builder builder() { return new AutoValue_GetDeviceSpecCommand.Builder().setOverwriteOutput(false); } @@ -107,6 +111,8 @@ public abstract static class Builder { public abstract Builder setDeviceGroups(ImmutableSet deviceGroups); + public abstract Builder setCountrySet(String countrySet); + abstract GetDeviceSpecCommand autoBuild(); public GetDeviceSpecCommand build() { @@ -144,6 +150,7 @@ public static GetDeviceSpecCommand fromFlags( DEVICE_TIER_FLAG.getValue(flags).ifPresent(builder::setDeviceTier); DEVICE_GROUPS_FLAG.getValue(flags).ifPresent(builder::setDeviceGroups); + COUNTRY_SET_FLAG.getValue(flags).ifPresent(builder::setCountrySet); flags.checkNoUnknownFlags(); return builder.build(); @@ -167,6 +174,10 @@ public DeviceSpec execute() { if (getDeviceGroups().isPresent()) { deviceSpec = deviceSpec.toBuilder().addAllDeviceGroups(getDeviceGroups().get()).build(); } + if (getCountrySet().isPresent()) { + deviceSpec = + deviceSpec.toBuilder().setCountrySet(StringValue.of(getCountrySet().get())).build(); + } writeDeviceSpecToFile(deviceSpec, getOutputPath()); return deviceSpec; } @@ -255,6 +266,17 @@ public static CommandHelp help() { + " This flag is only relevant if the bundle uses device group targeting" + " in conditional modules and should be set in that case.") .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(COUNTRY_SET_FLAG.getName()) + .setExampleValue("country_set_name") + .setOptional(true) + .setDescription( + "Country set for the user account on the device. This value will be" + + " used to match the correct country set targeted APKs to this device." + + " This flag is only relevant if the bundle uses country set targeting," + + " and should be set in that case.") + .build()) .build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java index 67a6cfc5..868a603e 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/GetSizeCommand.java @@ -115,6 +115,7 @@ public static GetSizeSubcommand fromString(String subCommand) { Dimension.SCREEN_DENSITY, Dimension.TEXTURE_COMPRESSION_FORMAT, Dimension.DEVICE_TIER, + Dimension.COUNTRY_SET, Dimension.SDK_RUNTIME); public abstract Path getApksArchivePath(); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java index 0f4c38b8..fb5d167b 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallApksCommand.java @@ -51,6 +51,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -72,6 +73,7 @@ public abstract class InstallApksCommand { private static final Flag DEVICE_TIER_FLAG = Flag.nonNegativeInteger("device-tier"); private static final Flag> DEVICE_GROUPS_FLAG = Flag.stringSet("device-groups"); + private static final Flag COUNTRY_SET_FLAG = Flag.string("country-set"); private static final Flag> ADDITIONAL_LOCAL_TESTING_FILES_FLAG = Flag.pathList("additional-local-testing-files"); private static final Flag TIMEOUT_MILLIS_FLAG = Flag.positiveInteger("timeout-millis"); @@ -95,6 +97,8 @@ public abstract class InstallApksCommand { public abstract Optional> getDeviceGroups(); + public abstract Optional getCountrySet(); + public abstract Optional> getAdditionalLocalTestingFiles(); abstract AdbServer getAdbServer(); @@ -130,6 +134,8 @@ public abstract static class Builder { public abstract Builder setDeviceGroups(ImmutableSet deviceGroups); + public abstract Builder setCountrySet(String countrySet); + public abstract Builder setAdditionalLocalTestingFiles(ImmutableList additionalFiles); public abstract Builder setTimeout(Duration timeout); @@ -154,6 +160,7 @@ public static InstallApksCommand fromFlags( Optional allowTestOnly = ALLOW_TEST_ONLY_FLAG.getValue(flags); Optional deviceTier = DEVICE_TIER_FLAG.getValue(flags); Optional> deviceGroups = DEVICE_GROUPS_FLAG.getValue(flags); + Optional countrySet = COUNTRY_SET_FLAG.getValue(flags); Optional> additionalLocalTestingFiles = ADDITIONAL_LOCAL_TESTING_FILES_FLAG.getValue(flags); Optional timeoutMillis = TIMEOUT_MILLIS_FLAG.getValue(flags); @@ -168,6 +175,7 @@ public static InstallApksCommand fromFlags( allowTestOnly.ifPresent(command::setAllowTestOnly); deviceTier.ifPresent(command::setDeviceTier); deviceGroups.ifPresent(command::setDeviceGroups); + countrySet.ifPresent(command::setCountrySet); additionalLocalTestingFiles.ifPresent(command::setAdditionalLocalTestingFiles); timeoutMillis.ifPresent(timeout -> command.setTimeout(Duration.ofMillis(timeout))); @@ -190,6 +198,10 @@ public void execute() { if (getDeviceGroups().isPresent()) { deviceSpec = deviceSpec.toBuilder().addAllDeviceGroups(getDeviceGroups().get()).build(); } + if (getCountrySet().isPresent()) { + deviceSpec = + deviceSpec.toBuilder().setCountrySet(StringValue.of(getCountrySet().get())).build(); + } final ImmutableList apksToInstall = getApksToInstall(toc, deviceSpec, tempDirectory.getPath()); @@ -302,8 +314,15 @@ private ImmutableList getApksToPushToStorage( ImmutableSet allModules = ExtractApksCommand.resolveRequestedModules( ImmutableSet.of(ExtractApksCommand.ALL_MODULES_SHORTCUT), toc); + + // We exclude install-time asset modules from the list of requested modules and also force + // the extract-apk layer to skip them explicitly. + // Install-time asset modules should not be pushed to storage, because they cannot be fetched + // through on-demand delivery. extractApksCommand.setModules( Sets.difference(allModules, installTimeAssetModules).immutableCopy()); + extractApksCommand.setIncludeInstallTimeAssetModules(false); + return extractApksCommand.build().execute().stream() .filter( apk -> @@ -452,6 +471,17 @@ public static CommandHelp help() { + " This flag is only relevant if the bundle uses device group targeting" + " in conditional modules and should be set in that case.") .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(COUNTRY_SET_FLAG.getName()) + .setExampleValue("country_set_name") + .setOptional(true) + .setDescription( + "Country set for the user account on the device. This value will be" + + " used to match the correct country set targeted APKs to this device." + + " This flag is only relevant if the bundle uses country set targeting," + + " and should be set in that case.") + .build()) .addFlag( FlagDescription.builder() .setFlagName(ADDITIONAL_LOCAL_TESTING_FILES_FLAG.getName()) diff --git a/src/main/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommand.java index ab2ecaf4..a772522e 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommand.java @@ -26,6 +26,7 @@ import com.android.bundle.DeviceTier; import com.android.bundle.DeviceTierConfig; import com.android.bundle.SystemFeature; +import com.android.bundle.UserCountrySet; 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; @@ -37,6 +38,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.io.PrintStream; @@ -56,6 +58,8 @@ public abstract class PrintDeviceTargetingConfigCommand { private static final Flag DEVICE_TARGETING_CONFIGURATION_LOCATION_FLAG = Flag.path("config"); + private static final int MAX_COUNTRY_CODES_ON_A_LINE = 10; + abstract Path getDeviceTargetingConfigurationPath(); abstract PrintStream getOutputStream(); @@ -98,6 +102,7 @@ public void execute() throws IOException { } printDeviceTierSet(config); + config.getUserCountrySetsList().forEach(countrySet -> printCountrySet(countrySet, "")); } } @@ -133,6 +138,24 @@ private void printDeviceTierSet(DeviceTierConfig config) { } } + private void printCountrySet(UserCountrySet countrySet, String indent) { + getOutputStream().println(indent + "Country set '" + countrySet.getName() + "':"); + getOutputStream().println(indent + INDENT + "("); + getOutputStream().println(indent + INDENT + INDENT + "Country Codes: ["); + printCountryCodes(countrySet.getCountryCodesList(), indent + INDENT + INDENT + INDENT); + getOutputStream().println(indent + INDENT + INDENT + "]"); + getOutputStream().println(indent + INDENT + ")"); + getOutputStream().println(); + } + + private void printCountryCodes(List countryCodes, String indent) { + List> partitionedCountryCodes = + Lists.partition(countryCodes, MAX_COUNTRY_CODES_ON_A_LINE); + partitionedCountryCodes.forEach( + countryCodesOnThisLine -> + getOutputStream().println(indent + String.join(", ", countryCodesOnThisLine))); + } + private void printDeviceTier( ImmutableSet selectorsFromGroupsInTier, ImmutableSet excludedSelectorsFromHigherTiers, diff --git a/src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java index 48aab398..3c4e313a 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java +++ b/src/main/java/com/android/tools/build/bundletool/device/AbstractSizeAggregator.java @@ -25,6 +25,7 @@ import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isSdkVersionMissing; import static com.android.tools.build.bundletool.device.DeviceSpecUtils.isTextureCompressionFormatMissing; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.ABI; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.COUNTRY_SET; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.DEVICE_TIER; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.LANGUAGE; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.SCREEN_DENSITY; @@ -39,6 +40,7 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensityTargeting; @@ -93,6 +95,7 @@ protected abstract ImmutableList getMatchingApks( LanguageTargeting languageTargeting, TextureCompressionFormatTargeting textureTargeting, DeviceTierTargeting deviceTierTargeting, + CountrySetTargeting countrySetTargeting, SdkRuntimeTargeting sdkRuntimeTargeting); protected ImmutableSet getAllSdkVersionTargetings( @@ -155,6 +158,16 @@ protected ImmutableSet getAllDeviceTierTargetings( DeviceTierTargeting.getDefaultInstance()); } + protected ImmutableSet getAllCountrySetTargetings( + ImmutableList apkDescriptions) { + return getAllTargetings( + apkDescriptions, + DeviceSpecUtils::isCountrySetMissing, + ApkTargeting::hasCountrySetTargeting, + ApkTargeting::getCountrySetTargeting, + CountrySetTargeting.getDefaultInstance()); + } + /** Retrieves all targetings for a generic dimension. */ protected ImmutableSet getAllTargetings( ImmutableList apkDescriptions, @@ -186,6 +199,7 @@ protected ConfigurationSizes getSizesPerConfiguration( ImmutableSet screenDensityTargetingOptions, ImmutableSet textureCompressionFormatTargetingOptions, ImmutableSet deviceTierTargetingOptions, + ImmutableSet countrySetTargetingOptions, // We use a single value instead of a set for SdkRuntimeTargeting since one variant can only // have one value for this dimension. SdkRuntimeTargeting sdkRuntimeTargeting) { @@ -199,32 +213,36 @@ protected ConfigurationSizes getSizesPerConfiguration( for (TextureCompressionFormatTargeting textureCompressionFormatTargeting : textureCompressionFormatTargetingOptions) { for (DeviceTierTargeting deviceTierTargeting : deviceTierTargetingOptions) { - - SizeConfiguration configuration = - mergeWithDeviceSpec( - getSizeConfiguration( - sdkVersionTargeting, - abiTargeting, - screenDensityTargeting, - languageTargeting, - textureCompressionFormatTargeting, - deviceTierTargeting, - sdkRuntimeTargeting), - getSizeRequest.getDeviceSpec()); - - long compressedSize = - getCompressedSize( - getMatchingApks( - sdkVersionTargeting, - abiTargeting, - screenDensityTargeting, - languageTargeting, - textureCompressionFormatTargeting, - deviceTierTargeting, - sdkRuntimeTargeting)); - - minSizeByConfiguration.merge(configuration, compressedSize, Math::min); - maxSizeByConfiguration.merge(configuration, compressedSize, Math::max); + for (CountrySetTargeting countrySetTargeting : countrySetTargetingOptions) { + + SizeConfiguration configuration = + mergeWithDeviceSpec( + getSizeConfiguration( + sdkVersionTargeting, + abiTargeting, + screenDensityTargeting, + languageTargeting, + textureCompressionFormatTargeting, + deviceTierTargeting, + countrySetTargeting, + sdkRuntimeTargeting), + getSizeRequest.getDeviceSpec()); + + long compressedSize = + getCompressedSize( + getMatchingApks( + sdkVersionTargeting, + abiTargeting, + screenDensityTargeting, + languageTargeting, + textureCompressionFormatTargeting, + deviceTierTargeting, + countrySetTargeting, + sdkRuntimeTargeting)); + + minSizeByConfiguration.merge(configuration, compressedSize, Math::min); + maxSizeByConfiguration.merge(configuration, compressedSize, Math::max); + } } } } @@ -244,6 +262,7 @@ protected SizeConfiguration getSizeConfiguration( LanguageTargeting languageTargeting, TextureCompressionFormatTargeting textureCompressionFormatTargeting, DeviceTierTargeting deviceTierTargeting, + CountrySetTargeting countrySetTargeting, SdkRuntimeTargeting sdkRuntimeTargeting) { ImmutableSet dimensions = getSizeRequest.getDimensions(); @@ -276,6 +295,11 @@ protected SizeConfiguration getSizeConfiguration( .ifPresent(sizeConfiguration::setDeviceTier); } + if (dimensions.contains(COUNTRY_SET)) { + SizeConfiguration.getCountrySetName(countrySetTargeting) + .ifPresent(sizeConfiguration::setCountrySet); + } + if (dimensions.contains(SDK_RUNTIME)) { sizeConfiguration.setSdkRuntime(SizeConfiguration.getSdkRuntimeRequired(sdkRuntimeTargeting)); } @@ -291,6 +315,7 @@ protected DeviceSpec getDeviceSpec( LanguageTargeting languageTargeting, TextureCompressionFormatTargeting textureTargeting, DeviceTierTargeting deviceTierTargeting, + CountrySetTargeting countrySetTargeting, SdkRuntimeTargeting sdkRuntimeTargeting) { return new DeviceSpecFromTargetingBuilder(deviceSpec) @@ -300,6 +325,7 @@ protected DeviceSpec getDeviceSpec( .setSupportedLocales(languageTargeting) .setSupportedTextureCompressionFormats(textureTargeting) .setDeviceTier(deviceTierTargeting) + .setCountrySet(countrySetTargeting) .setSdkRuntime(sdkRuntimeTargeting) .build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java index 8310462d..95de4679 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java +++ b/src/main/java/com/android/tools/build/bundletool/device/ApkMatcher.java @@ -55,6 +55,7 @@ public class ApkMatcher { private final Optional> requestedModuleNames; private final boolean matchInstant; + private final boolean includeInstallTimeAssetModules; private final ModuleMatcher moduleMatcher; private final VariantMatcher variantMatcher; private final boolean ensureDensityAndAbiApksMatched; @@ -63,6 +64,7 @@ public ApkMatcher(DeviceSpec deviceSpec) { this( deviceSpec, Optional.empty(), + /* includeInstallTimeAssetModules= */ true, /* matchInstant= */ false, /* ensureDensityAndAbiApksMatched= */ false); } @@ -80,6 +82,7 @@ public ApkMatcher(DeviceSpec deviceSpec) { public ApkMatcher( DeviceSpec deviceSpec, Optional> requestedModuleNames, + boolean includeInstallTimeAssetModules, boolean matchInstant, boolean ensureDensityAndAbiApksMatched) { checkArgument( @@ -95,6 +98,7 @@ public ApkMatcher( TextureCompressionFormatMatcher textureCompressionFormatMatcher = new TextureCompressionFormatMatcher(deviceSpec); DeviceTierApkMatcher deviceTierApkMatcher = new DeviceTierApkMatcher(deviceSpec); + CountrySetApkMatcher countrySetApkMatcher = new CountrySetApkMatcher(deviceSpec); DeviceGroupModuleMatcher deviceGroupModuleMatcher = new DeviceGroupModuleMatcher(deviceSpec); this.apkMatchers = @@ -105,8 +109,10 @@ public ApkMatcher( screenDensityMatcher, languageMatcher, textureCompressionFormatMatcher, - deviceTierApkMatcher); + deviceTierApkMatcher, + countrySetApkMatcher); this.requestedModuleNames = requestedModuleNames; + this.includeInstallTimeAssetModules = includeInstallTimeAssetModules; this.matchInstant = matchInstant; this.ensureDensityAndAbiApksMatched = ensureDensityAndAbiApksMatched; this.moduleMatcher = @@ -337,8 +343,12 @@ private static void checkCompatibleWithApkTargetingHelper( public ImmutableList getMatchingApksFromAssetModules( Collection assetModules) { - ImmutableSet assetModulesToMatch = - requestedModuleNames.orElseGet(() -> getUpfrontAssetModules(assetModules)); + Set assetModulesToMatch = + Sets.union( + requestedModuleNames.orElse(ImmutableSet.of()), + includeInstallTimeAssetModules + ? getUpfrontAssetModules(assetModules) + : ImmutableSet.of()); return assetModules.stream() .filter( diff --git a/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java index 739c73cb..5887e1db 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java +++ b/src/main/java/com/android/tools/build/bundletool/device/AssetModuleSizeAggregator.java @@ -21,6 +21,7 @@ import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.AssetSliceSet; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensityTargeting; @@ -87,6 +88,8 @@ public ConfigurationSizes getSize() { : getAllTextureCompressionFormatTargetings(apkDescriptions); ImmutableSet devicetierTargetingOptions = getAllDeviceTierTargetings(apkDescriptions); + ImmutableSet countrySetTargetingOptions = + getAllCountrySetTargetings(apkDescriptions); return getSizesPerConfiguration( sdkVersionTargetingOptions, @@ -95,6 +98,7 @@ public ConfigurationSizes getSize() { screenDensityTargetingOptions, textureCompressionFormatTargetingOptions, devicetierTargetingOptions, + countrySetTargetingOptions, variantTargeting.getSdkRuntimeTargeting()); } @@ -106,6 +110,7 @@ protected ImmutableList getMatchingApks( LanguageTargeting languageTargeting, TextureCompressionFormatTargeting textureTargeting, DeviceTierTargeting deviceTierTargeting, + CountrySetTargeting countrySetTargeting, SdkRuntimeTargeting sdkRuntimeTargeting) { return new ApkMatcher( getDeviceSpec( @@ -116,8 +121,10 @@ protected ImmutableList getMatchingApks( languageTargeting, textureTargeting, deviceTierTargeting, + countrySetTargeting, sdkRuntimeTargeting), getSizeRequest.getModules(), + /* includeInstallTimeAssetModules= */ true, getSizeRequest.getInstant(), /* ensureDensityAndAbiApksMatched= */ false) .getMatchingApksFromAssetModules(assetModules); diff --git a/src/main/java/com/android/tools/build/bundletool/device/CountrySetApkMatcher.java b/src/main/java/com/android/tools/build/bundletool/device/CountrySetApkMatcher.java new file mode 100644 index 00000000..28010785 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/device/CountrySetApkMatcher.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 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.device; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; +import com.android.bundle.Targeting.VariantTargeting; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.common.collect.Sets.SetView; +import com.google.common.collect.Streams; + +/** + * A {@link TargetingDimensionMatcher} that provides APK matching on country set. + * + *

Country Set is a user attribute and it is explicitly defined in the {@link DeviceSpec}. + */ +public class CountrySetApkMatcher extends TargetingDimensionMatcher { + + public CountrySetApkMatcher(DeviceSpec deviceSpec) { + super(deviceSpec); + } + + @Override + protected CountrySetTargeting getTargetingValue(ApkTargeting apkTargeting) { + return apkTargeting.getCountrySetTargeting(); + } + + @Override + protected CountrySetTargeting getTargetingValue(VariantTargeting variantTargeting) { + // Country set is not propagated to the variant targeting. + return CountrySetTargeting.getDefaultInstance(); + } + + @Override + public boolean matchesTargeting(CountrySetTargeting targeting) { + // If there is no targeting, by definition the targeting is matched. + if (targeting.equals(CountrySetTargeting.getDefaultInstance())) { + return true; + } + + ImmutableSet values = ImmutableSet.copyOf(targeting.getValueList()); + ImmutableSet alternatives = ImmutableSet.copyOf(targeting.getAlternativesList()); + + SetView intersection = Sets.intersection(values, alternatives); + checkArgument( + intersection.isEmpty(), + "Expected targeting values and alternatives to be mutually exclusive, but both contain: %s", + intersection); + + // case where country set is not specified in device spec and default suffix is "". + if (!getDeviceSpec().hasCountrySet() || getDeviceSpec().getCountrySet().getValue().isEmpty()) { + return values.isEmpty() && !alternatives.isEmpty(); + } + + return values.contains(getDeviceSpec().getCountrySet().getValue()); + } + + @Override + protected boolean isDeviceDimensionPresent() { + return getDeviceSpec().hasCountrySet(); + } + + @Override + protected void checkDeviceCompatibleInternal(CountrySetTargeting targeting) { + if (targeting.equals(CountrySetTargeting.getDefaultInstance())) { + return; + } + if (!getDeviceSpec().hasCountrySet() || getDeviceSpec().getCountrySet().getValue().isEmpty()) { + // If no country set is specified in device spec, fallback or default suffix targeting + // apks are used. + return; + } + ImmutableSet valuesAndAlternatives = + Streams.concat(targeting.getValueList().stream(), targeting.getAlternativesList().stream()) + .collect(toImmutableSet()); + checkArgument( + valuesAndAlternatives.contains(getDeviceSpec().getCountrySet().getValue()), + "The specified country set '%s' does not match any of the available values: %s.", + getDeviceSpec().getCountrySet().getValue(), + String.join(", ", valuesAndAlternatives)); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/device/DeviceSpecUtils.java b/src/main/java/com/android/tools/build/bundletool/device/DeviceSpecUtils.java index b1885f3e..98c74de6 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/DeviceSpecUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DeviceSpecUtils.java @@ -23,6 +23,7 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Devices.SdkRuntime; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceFeatureTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; @@ -40,6 +41,7 @@ import com.google.common.collect.Streams; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import java.util.Optional; /** Utils for {@link DeviceSpec}. */ @@ -70,6 +72,10 @@ public static boolean isDeviceTierMissing(DeviceSpec deviceSpec) { return !deviceSpec.hasDeviceTier(); } + public static boolean isCountrySetMissing(DeviceSpec deviceSpec) { + return !deviceSpec.hasCountrySet(); + } + public static boolean isSdkRuntimeUnspecified(DeviceSpec deviceSpec) { return !deviceSpec.hasSdkRuntime(); } @@ -183,6 +189,15 @@ DeviceSpecFromTargetingBuilder setDeviceTier(DeviceTierTargeting deviceTierTarge return this; } + @CanIgnoreReturnValue + DeviceSpecFromTargetingBuilder setCountrySet(CountrySetTargeting countrySetTargeting) { + if (!countrySetTargeting.equals(CountrySetTargeting.getDefaultInstance())) { + deviceSpec.setCountrySet( + StringValue.of(Iterables.getOnlyElement(countrySetTargeting.getValueList(), ""))); + } + return this; + } + @CanIgnoreReturnValue DeviceSpecFromTargetingBuilder setSdkRuntime(SdkRuntimeTargeting sdkRuntimeTargeting) { deviceSpec.setSdkRuntime( diff --git a/src/main/java/com/android/tools/build/bundletool/device/DeviceTargetingConfigEvaluator.java b/src/main/java/com/android/tools/build/bundletool/device/DeviceTargetingConfigEvaluator.java index e4cd494c..b389941b 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/DeviceTargetingConfigEvaluator.java +++ b/src/main/java/com/android/tools/build/bundletool/device/DeviceTargetingConfigEvaluator.java @@ -28,6 +28,7 @@ import com.android.bundle.DeviceSelector; import com.android.bundle.DeviceTier; import com.android.bundle.DeviceTierConfig; +import com.android.bundle.UserCountrySet; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -92,6 +93,15 @@ public static ImmutableSet getDeviceSelectorsInTier( .collect(toImmutableSet()); } + /** Get the {@link UserCountrySet} that matches the provided user country. */ + public static String getMatchingCountrySet(DeviceTierConfig config, String countryCode) { + return config.getUserCountrySetsList().stream() + .filter(countrySet -> countrySet.getCountryCodesList().contains(countryCode)) + .map(UserCountrySet::getName) + .findAny() + .orElse(""); + } + private static boolean devicePropertiesMatchDeviceGroup( DeviceProperties deviceProperties, DeviceGroup deviceGroup) { return deviceGroup.getDeviceSelectorsList().stream() diff --git a/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java b/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java index 55bd9abb..d5caca93 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java +++ b/src/main/java/com/android/tools/build/bundletool/device/VariantTotalSizeAggregator.java @@ -23,6 +23,7 @@ import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.Variant; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensityTargeting; @@ -77,6 +78,7 @@ protected ImmutableList getMatchingApks( LanguageTargeting languageTargeting, TextureCompressionFormatTargeting textureTargeting, DeviceTierTargeting deviceTierTargeting, + CountrySetTargeting countrySetTargeting, SdkRuntimeTargeting sdkRuntimeTargeting) { return new ApkMatcher( getDeviceSpec( @@ -87,8 +89,10 @@ protected ImmutableList getMatchingApks( languageTargeting, textureTargeting, deviceTierTargeting, + countrySetTargeting, sdkRuntimeTargeting), getSizeRequest.getModules(), + /* includeInstallTimeAssetModules= */ true, getSizeRequest.getInstant(), /* ensureDensityAndAbiApksMatched= */ false) .getMatchingApksFromVariant(variant, bundleVersion); @@ -111,6 +115,8 @@ private ConfigurationSizes getSizeNonStandaloneVariant() { getAllTextureCompressionFormatTargetings(apkDescriptions); ImmutableSet deviceTierTargetingOptions = getAllDeviceTierTargetings(apkDescriptions); + ImmutableSet countrySetTargetingOptions = + getAllCountrySetTargetings(apkDescriptions); return getSizesPerConfiguration( sdkVersionTargetingOptions, @@ -119,6 +125,7 @@ private ConfigurationSizes getSizeNonStandaloneVariant() { screenDensityTargetingOptions, textureCompressionFormatTargetingOptions, deviceTierTargetingOptions, + countrySetTargetingOptions, variant.getTargeting().getSdkRuntimeTargeting()); } @@ -143,6 +150,7 @@ private ConfigurationSizes getSizeStandaloneVariant() { LanguageTargeting.getDefaultInstance(), variantTargeting.getTextureCompressionFormatTargeting(), DeviceTierTargeting.getDefaultInstance(), + CountrySetTargeting.getDefaultInstance(), variantTargeting.getSdkRuntimeTargeting()), getSizeRequest.getDeviceSpec()); 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 e205a4dd..dd1c6b09 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 @@ -81,6 +81,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; @@ -219,7 +220,9 @@ ImmutableList serializeApks( // To avoid filtering of unmatched language splits we skip device filtering for system mode. Predicate deviceFilter = deviceSpec.isPresent() && !apkBuildMode.equals(SYSTEM) - ? new ApkMatcher(addDefaultDeviceTierIfNecessary(deviceSpec.get())) + ? new ApkMatcher( + addDefaultCountrySetIfNecessary( + addDefaultDeviceTierIfNecessary(deviceSpec.get()))) ::matchesModuleSplitByTargeting : alwaysTrue(); @@ -303,7 +306,9 @@ ImmutableList serializeAssetSlices( Predicate deviceFilter = deviceSpec.isPresent() - ? new ApkMatcher(addDefaultDeviceTierIfNecessary(deviceSpec.get())) + ? new ApkMatcher( + addDefaultCountrySetIfNecessary( + addDefaultDeviceTierIfNecessary(deviceSpec.get()))) ::matchesModuleSplitByTargeting : alwaysTrue(); @@ -524,4 +529,26 @@ private DeviceSpec addDefaultDeviceTierIfNecessary(DeviceSpec deviceSpec) { .orElse(0))) .build(); } + + /** + * Adds a default country set to the given {@link DeviceSpec} if it has none. + * + *

The default country set is taken from the optimization settings in the {@link + * com.android.bundle.Config.BundleConfig}. + */ + private DeviceSpec addDefaultCountrySetIfNecessary(DeviceSpec deviceSpec) { + if (deviceSpec.hasCountrySet()) { + return deviceSpec; + } + Optional countrySetSuffix = + Optional.ofNullable( + apkOptimizations.getSuffixStrippings().get(OptimizationDimension.COUNTRY_SET)); + if (!countrySetSuffix.isPresent()) { + return deviceSpec; + } + return deviceSpec.toBuilder() + .setCountrySet( + StringValue.of(countrySetSuffix.map(SuffixStripping::getDefaultSuffix).orElse(""))) + .build(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java b/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java index cac68a5d..0bd9126d 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ConcurrencyUtils.java @@ -16,6 +16,8 @@ package com.android.tools.build.bundletool.io; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + import com.android.tools.build.bundletool.model.exceptions.BundleToolException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -31,8 +33,14 @@ /** Utility methods for working with concurrent code. */ final class ConcurrencyUtils { - /** Retrieves results of all futures, if they succeed. If any fails, eagerly throws. */ + /** Retrieves results of all futures, if they succeed. If any fails, throws. */ public static ImmutableList waitForAll(Iterable> futures) { + // Note that all futures are already completed due to the Futures#successfulAsList(), however + // it means that some futures might fail, and we want to propagate a failure down the line. The + // reason that we don't use Futures#allAsList() directly is it will fail-fast and will cause the + // other existing futures to continue running until they fail (e.g. if they depend on the + // temporary file which gets deleted then it will produce NoFileFound exception). + waitFor(Futures.whenAllComplete(futures).call(() -> null, directExecutor())); return ImmutableList.copyOf(waitFor(Futures.allAsList(futures))); } @@ -44,7 +52,7 @@ public static ImmutableMap waitForAll(Map> f return finishedMap.build(); } - public static T waitFor(Future future) { + private static T waitFor(Future future) { try { return future.get(); } catch (ExecutionException e) { diff --git a/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java b/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java index 1be3e14f..3296e741 100644 --- a/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/mergers/MergingUtils.java @@ -18,6 +18,8 @@ import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.abiUniverse; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.abiValues; +import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.countrySetUniverse; +import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.countrySetValues; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.densityUniverse; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.densityValues; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.deviceTierUniverse; @@ -35,6 +37,7 @@ import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.MultiAbi; @@ -85,6 +88,7 @@ public static Optional getSameValueOrNonNull(@Nullable T nullableValue, T *

  • Language *
  • Texture compression format *
  • Device tier + *
  • Country Set * * *

    If both targetings target a common dimension, then the targeted universe in that dimension @@ -121,6 +125,10 @@ public static ApkTargeting mergeShardTargetings( merged.setDeviceTierTargeting(mergeDeviceTierTargetingsOf(targeting1, targeting2)); } + if (targeting1.hasCountrySetTargeting() || targeting2.hasCountrySetTargeting()) { + merged.setCountrySetTargeting(mergeCountrySetTargetingsOf(targeting1, targeting2)); + } + return merged.build(); } @@ -144,7 +152,7 @@ public static void mergeTargetedAssetsDirectories( } private static void checkTargetingIsSupported(ApkTargeting targeting) { - ApkTargeting targetingWithoutAbiDensityLanguageAndTcf = + ApkTargeting targetingOtherThanSupportedDimensions = targeting.toBuilder() .clearAbiTargeting() .clearMultiAbiTargeting() @@ -152,12 +160,13 @@ private static void checkTargetingIsSupported(ApkTargeting targeting) { .clearLanguageTargeting() .clearTextureCompressionFormatTargeting() .clearDeviceTierTargeting() + .clearCountrySetTargeting() .build(); - if (!targetingWithoutAbiDensityLanguageAndTcf.equals(ApkTargeting.getDefaultInstance())) { + if (!targetingOtherThanSupportedDimensions.equals(ApkTargeting.getDefaultInstance())) { throw CommandExecutionException.builder() .withInternalMessage( - "Expecting only ABI, screen density, language and texture compression format" - + " targeting, got '%s'.", + "Expecting only ABI, screen density, language, texture compression format, device" + + " tier and country set targeting, got '%s'.", targeting) .build(); } @@ -231,5 +240,17 @@ private static DeviceTierTargeting mergeDeviceTierTargetingsOf( .build(); } + private static CountrySetTargeting mergeCountrySetTargetingsOf( + ApkTargeting targeting1, ApkTargeting targeting2) { + Set universe = + Sets.union(countrySetUniverse(targeting1), countrySetUniverse(targeting2)); + Set values = Sets.union(countrySetValues(targeting1), countrySetValues(targeting2)); + Set alternatives = Sets.difference(universe, values); + return CountrySetTargeting.newBuilder() + .addAllValue(values) + .addAllAlternatives(alternatives) + .build(); + } + private MergingUtils() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/GetSizeRequest.java b/src/main/java/com/android/tools/build/bundletool/model/GetSizeRequest.java index 913f6835..0995871d 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/GetSizeRequest.java +++ b/src/main/java/com/android/tools/build/bundletool/model/GetSizeRequest.java @@ -31,6 +31,7 @@ enum Dimension { LANGUAGE, TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER, + COUNTRY_SET, SDK_RUNTIME, ALL } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java index 6806e77f..2e301b86 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleSplit.java @@ -33,6 +33,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.MoreCollectors.toOptional; +import static java.nio.charset.StandardCharsets.UTF_8; import com.android.aapt.Resources.ResourceTable; import com.android.bundle.Config.ApexEmbeddedApkConfig; @@ -43,6 +44,7 @@ import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.MultiAbi; import com.android.bundle.Targeting.MultiAbiTargeting; @@ -53,6 +55,7 @@ import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.model.SourceStamp.StampType; +import com.android.tools.build.bundletool.model.targeting.TargetedDirectorySegment; import com.android.tools.build.bundletool.model.utils.ResourcesUtils; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; @@ -222,6 +225,16 @@ public String getSuffix() { .getValueList() .forEach(value -> suffixJoiner.add("tier_" + value.getValue())); + CountrySetTargeting countrySetTargeting = getApkTargeting().getCountrySetTargeting(); + if (!countrySetTargeting.getValueList().isEmpty()) { + countrySetTargeting + .getValueList() + .forEach( + value -> suffixJoiner.add(TargetedDirectorySegment.COUNTRY_SET_KEY + "_" + value)); + } else if (!countrySetTargeting.getAlternativesList().isEmpty()) { + suffixJoiner.add("other_countries"); + } + return suffixJoiner.toString(); } @@ -378,11 +391,23 @@ public ModuleSplit writeSdkProviderClassName(String sdkProviderClassName) { return toBuilder().setAndroidManifest(apkManifest).build(); } - /** Writes the compatibility SDK provider class name to a new element. */ + /** + * Writes the compatibility SDK provider class name to a new element in the manifest, + * as well as to a text file under assets/ directory. + */ public ModuleSplit writeCompatSdkProviderClassName(String sdkProviderClassName) { AndroidManifest apkManifest = getAndroidManifest().toEditor().setCompatSdkProviderClassName(sdkProviderClassName).save(); - return toBuilder().setAndroidManifest(apkManifest).build(); + return toBuilder() + .setAndroidManifest(apkManifest) + // This is a workaround for a platform bug which does not let the compat library parse the + // class name from the manifest. + .addEntry( + ModuleEntry.builder() + .setPath(ZipPath.create("assets/SandboxedSdkProviderCompatClassName.txt")) + .setContent(ByteSource.wrap(sdkProviderClassName.getBytes(UTF_8))) + .build()) + .build(); } /** @@ -759,10 +784,11 @@ public ModuleSplit build() { // was enabled, a default targeting suffix was used. .clearTextureCompressionFormatTargeting() .clearDeviceTierTargeting() + .clearCountrySetTargeting() .build() .equals(ApkTargeting.getDefaultInstance()), "Master split cannot have any targeting other than SDK version, Texture" - + "Compression Format and Device Tier."); + + " Compression Format, Device Tier and Country Set."); } return moduleSplit; } diff --git a/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java b/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java index 0ee1c7e4..445b0c6f 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java +++ b/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java @@ -22,5 +22,6 @@ public enum OptimizationDimension { SCREEN_DENSITY, LANGUAGE, TEXTURE_COMPRESSION_FORMAT, - DEVICE_TIER + DEVICE_TIER, + COUNTRY_SET } diff --git a/src/main/java/com/android/tools/build/bundletool/model/SizeConfiguration.java b/src/main/java/com/android/tools/build/bundletool/model/SizeConfiguration.java index a80a2171..5238b426 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/SizeConfiguration.java +++ b/src/main/java/com/android/tools/build/bundletool/model/SizeConfiguration.java @@ -21,6 +21,7 @@ import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getMinSdk; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensity; @@ -55,6 +56,8 @@ public abstract class SizeConfiguration { public abstract Optional getDeviceTier(); + public abstract Optional getCountrySet(); + public abstract Optional getSdkRuntime(); public abstract Builder toBuilder(); @@ -108,6 +111,9 @@ public static Optional getScreenDensityName( public static Optional getTextureCompressionFormatName( TextureCompressionFormatTargeting textureCompressionFormatTargeting) { if (textureCompressionFormatTargeting.getValueList().isEmpty()) { + if (!textureCompressionFormatTargeting.getAlternativesList().isEmpty()) { + return Optional.of(""); + } return Optional.empty(); } return Optional.of( @@ -122,6 +128,17 @@ public static Optional getDeviceTierLevel(DeviceTierTargeting deviceTie return Optional.of(Iterables.getOnlyElement(deviceTierTargeting.getValueList()).getValue()); } + public static Optional getCountrySetName(CountrySetTargeting countrySetTargeting) { + if (countrySetTargeting.getValueList().isEmpty()) { + if (!countrySetTargeting.getAlternativesList().isEmpty()) { + // Case of fallback folder, country set name is empty string targeting rest of world + return Optional.of(""); + } + return Optional.empty(); + } + return Optional.of(Iterables.getOnlyElement(countrySetTargeting.getValueList())); + } + /** * Returns String indicating the targeting requires the SDK runtime to be supported on the device. */ @@ -144,6 +161,8 @@ public abstract static class Builder { public abstract Builder setDeviceTier(Integer deviceTier); + public abstract Builder setCountrySet(String countrySet); + public abstract Builder setSdkRuntime(String required); public abstract SizeConfiguration build(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java index b308572d..74a78839 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegment.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; @@ -44,10 +45,12 @@ @AutoValue.CopyAnnotations public abstract class TargetedDirectorySegment { + public static final String COUNTRY_SET_KEY = "countries"; + private static final String COUNTRY_SET_NAME_REGEX_STRING = "^[a-zA-Z][a-zA-Z0-9_]*$"; private static final Pattern DIRECTORY_SEGMENT_PATTERN = Pattern.compile("(?.+?)#(?.+?)_(?.+)"); - private static final Pattern LANGUAGE_CODE_PATTERN = Pattern.compile("^[a-zA-Z]{2,3}$"); + private static final Pattern COUNTRY_SET_PATTERN = Pattern.compile(COUNTRY_SET_NAME_REGEX_STRING); private static final String LANG_KEY = "lang"; private static final String TCF_KEY = "tcf"; @@ -58,11 +61,11 @@ public abstract class TargetedDirectorySegment { .put(LANG_KEY, TargetingDimension.LANGUAGE) .put(TCF_KEY, TargetingDimension.TEXTURE_COMPRESSION_FORMAT) .put(DEVICE_TIER_KEY, TargetingDimension.DEVICE_TIER) + .put(COUNTRY_SET_KEY, TargetingDimension.COUNTRY_SET) .build(); private static final ImmutableSetMultimap DIMENSION_TO_KEY = KEY_TO_DIMENSION.asMultimap().inverse(); - public abstract String getName(); /** Positive targeting resolved from this directory name. */ @@ -94,6 +97,8 @@ && getTargeting().hasTextureCompressionFormat()) { newTargeting.clearTextureCompressionFormat(); } else if (dimension.equals(TargetingDimension.DEVICE_TIER) && getTargeting().hasDeviceTier()) { newTargeting.clearDeviceTier(); + } else if (dimension.equals(TargetingDimension.COUNTRY_SET) && getTargeting().hasCountrySet()) { + newTargeting.clearCountrySet(); } else { // Nothing to remove, return the existing immutable object. return this; @@ -167,6 +172,8 @@ private static Optional getTargetingKey(AssetsDirectoryTargeting targeti return Optional.of(TCF_KEY); } else if (targeting.hasDeviceTier()) { return Optional.of(DEVICE_TIER_KEY); + } else if (targeting.hasCountrySet()) { + return Optional.of(COUNTRY_SET_KEY); } return Optional.empty(); @@ -190,6 +197,8 @@ private static Optional getTargetingValue(AssetsDirectoryTargeting targe return Optional.of( Integer.toString( Iterables.getOnlyElement(targeting.getDeviceTier().getValueList()).getValue())); + } else if (targeting.hasCountrySet()) { + return Optional.of(Iterables.getOnlyElement(targeting.getCountrySet().getValueList())); } return Optional.empty(); @@ -211,6 +220,8 @@ private static AssetsDirectoryTargeting toAssetsDirectoryTargeting( return parseTextureCompressionFormat(name, value); case DEVICE_TIER: return parseDeviceTier(name, value); + case COUNTRY_SET: + return parseCountrySet(name, value); default: throw InvalidBundleException.builder() .withUserMessage("Unrecognized key: '%s'.", key) @@ -253,4 +264,18 @@ private static AssetsDirectoryTargeting parseDeviceTier(String name, String valu DeviceTierTargeting.newBuilder().addValue(Int32Value.of(Integer.parseInt(value)))) .build(); } + + private static AssetsDirectoryTargeting parseCountrySet(String name, String value) { + Matcher matcher = COUNTRY_SET_PATTERN.matcher(value); + if (!matcher.matches()) { + throw InvalidBundleException.builder() + .withUserMessage( + "Country set name should match the regex '%s' but got '%s' for directory '%s'.", + COUNTRY_SET_NAME_REGEX_STRING, value, name) + .build(); + } + return AssetsDirectoryTargeting.newBuilder() + .setCountrySet(CountrySetTargeting.newBuilder().addValue(value)) + .build(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingDimension.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingDimension.java index 8ef767da..23faf1f5 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingDimension.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingDimension.java @@ -16,12 +16,11 @@ package com.android.tools.build.bundletool.model.targeting; -/** - * A dimension recognized by the bundletool for targeting. - */ +/** A dimension recognized by the bundletool for targeting. */ public enum TargetingDimension { ABI, LANGUAGE, TEXTURE_COMPRESSION_FORMAT, - DEVICE_TIER; + DEVICE_TIER, + COUNTRY_SET; } diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java index 16fc89c1..d3ae1d33 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingUtils.java @@ -66,6 +66,9 @@ public static ImmutableList getTargetingDimensions( if (targeting.hasDeviceTier()) { dimensions.add(TargetingDimension.DEVICE_TIER); } + if (targeting.hasCountrySet()) { + dimensions.add(TargetingDimension.COUNTRY_SET); + } return dimensions.build(); } @@ -275,6 +278,21 @@ public static Optional extractDeviceTier(TargetedDirectory targetedDire .collect(toOptional())); } + /** + * Extracts all the country sets used in the targeted directories. + * + *

    This is only useful when BundleModule assets config is not yet generated (which is the case + * when validators are run). Prefer using {@link BundleModule#getAssetsConfig} for all other + * cases. + */ + public static ImmutableSet extractCountrySets( + ImmutableSet targetedDirectories) { + return targetedDirectories.stream() + .map(TargetingUtils::extractCountrySet) + .flatMap(Streams::stream) + .collect(toImmutableSet()); + } + public static Optional generateAssetsTargeting(BundleModule module) { ImmutableList assetDirectories = module @@ -336,4 +354,11 @@ public static Optional generateApexImagesTargeting(BundleModule modu return Optional.of( new TargetingGenerator().generateTargetingForApexImages(apexImageFiles, hasBuildInfo)); } + + private static Optional extractCountrySet(TargetedDirectory targetedDirectory) { + return targetedDirectory + .getTargeting(TargetingDimension.COUNTRY_SET) + .flatMap( + targeting -> targeting.getCountrySet().getValueList().stream().collect(toOptional())); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java index 5d906693..c0344656 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMerger.java @@ -65,6 +65,7 @@ && areCompatible(sizeConfig1.getSdkVersion(), sizeConfig2.getSdkVersion()) && areCompatible( sizeConfig1.getTextureCompressionFormat(), sizeConfig2.getTextureCompressionFormat()) && areCompatible(sizeConfig1.getDeviceTier(), sizeConfig2.getDeviceTier()) + && areCompatible(sizeConfig1.getCountrySet(), sizeConfig2.getCountrySet()) && areCompatible(sizeConfig1.getSdkRuntime(), sizeConfig2.getSdkRuntime()); } @@ -96,6 +97,7 @@ private static Map.Entry combineEntries( .getTextureCompressionFormat() .ifPresent(configBuilder::setTextureCompressionFormat); entry2.getKey().getDeviceTier().ifPresent(configBuilder::setDeviceTier); + entry2.getKey().getCountrySet().ifPresent(configBuilder::setCountrySet); entry2.getKey().getSdkRuntime().ifPresent(configBuilder::setSdkRuntime); return Maps.immutableEntry(configBuilder.build(), entry1.getValue() + entry2.getValue()); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtils.java index 37ea4551..79482d07 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtils.java @@ -41,6 +41,7 @@ public final class GetSizeCsvUtils { Dimension.LANGUAGE, Dimension.TEXTURE_COMPRESSION_FORMAT, Dimension.DEVICE_TIER, + Dimension.COUNTRY_SET, Dimension.SDK_RUNTIME); public static String getSizeTotalOutputInCsv( @@ -96,6 +97,7 @@ private static ImmutableList getSizeTotalCsvRow( .put( Dimension.DEVICE_TIER, () -> sizeConfiguration.getDeviceTier().map(i -> i.toString())) + .put(Dimension.COUNTRY_SET, sizeConfiguration::getCountrySet) .put(Dimension.SDK_RUNTIME, sizeConfiguration::getSdkRuntime) .build(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java index 03b7cc92..37b5f4ea 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java @@ -17,12 +17,14 @@ package com.android.tools.build.bundletool.model.utils; import static com.google.common.collect.Comparators.lexicographical; +import static com.google.common.collect.ImmutableList.sortedCopyOf; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Comparator.comparing; import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.MultiAbi; @@ -92,6 +94,10 @@ public static ApkTargeting normalizeApkTargeting(ApkTargeting targeting) { normalized.setDeviceTierTargeting( normalizeDeviceTierTargeting(targeting.getDeviceTierTargeting())); } + if (targeting.hasCountrySetTargeting()) { + normalized.setCountrySetTargeting( + normalizeCountrySetTargeting(targeting.getCountrySetTargeting())); + } return normalized.build(); } @@ -198,6 +204,13 @@ private static DeviceTierTargeting normalizeDeviceTierTargeting(DeviceTierTarget .build(); } + private static CountrySetTargeting normalizeCountrySetTargeting(CountrySetTargeting targeting) { + return CountrySetTargeting.newBuilder() + .addAllValue(sortedCopyOf(targeting.getValueList())) + .addAllAlternatives(sortedCopyOf(targeting.getAlternativesList())) + .build(); + } + private static ImmutableList sortInt32Values(Collection values) { return values.stream() .map(Int32Value::getValue) diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java index 10134d57..023f6490 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtils.java @@ -61,6 +61,11 @@ public static AssetsDirectoryTargeting toAlternativeTargeting( .getDeviceTierBuilder() .addAllAlternatives(targeting.getDeviceTier().getValueList()); } + if (targeting.hasCountrySet()) { + alternativeTargeting + .getCountrySetBuilder() + .addAllAlternatives(targeting.getCountrySet().getValueList()); + } return alternativeTargeting.build(); } @@ -177,6 +182,17 @@ public static ImmutableSet deviceTierUniverse(ApkTargeting targeting) { .collect(toImmutableSet()); } + public static ImmutableSet countrySetUniverse(ApkTargeting targeting) { + return Streams.concat( + targeting.getCountrySetTargeting().getValueList().stream(), + targeting.getCountrySetTargeting().getAlternativesList().stream()) + .collect(toImmutableSet()); + } + + public static ImmutableSet countrySetValues(ApkTargeting targeting) { + return ImmutableSet.copyOf(targeting.getCountrySetTargeting().getValueList()); + } + public static SdkVersion sdkVersionFrom(int from) { return SdkVersion.newBuilder().setMin(Int32Value.newBuilder().setValue(from)).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 011a6ebc..0a2feeac 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.0"; + private static final String CURRENT_VERSION = "1.13.1"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java b/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java index d87e35c7..ddb658b8 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java @@ -93,13 +93,7 @@ public enum VersionGuardedFeature { * Install time modules will be merged into base unless explicitly turned off via in "install-time" attribute. */ - MERGE_INSTALL_TIME_MODULES_INTO_BASE("1.0.0"), - - /* Enabling generation of archived apk. */ - ARCHIVED_APK_GENERATION("1.8.2"), - - /* StoreArchive setting is enabled if not explicitly disabled in BundleConfig. */ - STORE_ARCHIVE_ENABLED_BY_DEFAULT("1.10.0"); + MERGE_INSTALL_TIME_MODULES_INTO_BASE("1.0.0"); /** Version from which the given feature should be enabled by default. */ private final Version enabledSinceVersion; diff --git a/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java b/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java index fe5fb92e..5e502abc 100644 --- a/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java +++ b/src/main/java/com/android/tools/build/bundletool/optimizations/ApkOptimizations.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.optimizations; import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; +import static com.android.tools.build.bundletool.model.OptimizationDimension.COUNTRY_SET; import static com.android.tools.build.bundletool.model.OptimizationDimension.DEVICE_TIER; import static com.android.tools.build.bundletool.model.OptimizationDimension.LANGUAGE; import static com.android.tools.build.bundletool.model.OptimizationDimension.SCREEN_DENSITY; @@ -86,7 +87,7 @@ public abstract class ApkOptimizations { /** List of dimensions supported by asset modules. */ private static final ImmutableSet DIMENSIONS_SUPPORTED_BY_ASSET_MODULES = - ImmutableSet.of(LANGUAGE, TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER); + ImmutableSet.of(LANGUAGE, TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER, COUNTRY_SET); public abstract ImmutableSet getSplitDimensions(); diff --git a/src/main/java/com/android/tools/build/bundletool/recovery/dex/update.dex b/src/main/java/com/android/tools/build/bundletool/recovery/dex/update.dex new file mode 100644 index 00000000..f7c6436a Binary files /dev/null and b/src/main/java/com/android/tools/build/bundletool/recovery/dex/update.dex differ 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 14e2b5a3..ada84aba 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 @@ -41,28 +41,38 @@ public final class DexAndResourceRepackager { private static final String COMPAT_CONFIG_ELEMENT_NAME = "compat-config"; private static final String COMPAT_ENTRYPOINT_ELEMENT_NAME = "compat-entrypoint"; private static final String DEX_PATH_ELEMENT_NAME = "dex-path"; - private static final String JAVA_RESOURCE_PATH_ELEMENT_NAME = "java-resource-path"; + private static final String JAVA_RESOURCES_ROOT_PATH_ELEMENT_NAME = "java-resources-root-path"; // Element which contains the package ID that SDK resource IDs should be remapped to. private static final String RESOURCES_PACKAGE_ID_ELEMENT_NAME = "resources-package-id"; // Element which contains the fully qualified name of the RPackage class of the SDK, where the new // resources package ID should be set at app runtime. private static final String R_PACKAGE_CLASS_NAME_ELEMENT_NAME = "r-package-class"; + // Parent element to RESOURCES_PACKAGE_ID_ELEMENT_NAME and R_PACKAGE_CLASS_NAME_ELEMENT_NAME. + private static final String RESOURCE_ID_REMAPPING_ELEMENT_NAME = "resource-id-remapping"; private static final String R_PACKAGE_CLASS_NAME = "RPackage"; /** - * Name of the config file that contains paths to moved dex files and java resources inside assets - * folder, as well as the path to the SDK entrypoint class. Example of what CompatSdkConfig.xml - * contents look like: + * Name of the config file that contains: + * + *

      + *
    • paths to moved dex files and java resources inside assets. + *
    • path to the SDK entrypoint class. + *
    • metadata necessary for SDK resource ID remapping. + *
    + * + * Example of what CompatSdkConfig.xml contents look like: * *
    {@code
        * 
        *   RuntimeEnabledSdk-sdk.package.name/classes.dex
        *   RuntimeEnabledSdk-sdk.package.name/classes2.dex
    -   *   RuntimeEnabledSdk-sdk.package.name/image.png
    +   *   RuntimeEnabledSdk-sdk.package.name
        *   com.sdk.EntryPointClass
    -   *   123
    -   *   sdk.package.name.RPackage
    +   *   
    +   *     123
    +   *     sdk.package.name.RPackage
    +   *   
        * 
        * }
    */ @@ -133,10 +143,9 @@ private Document getCompatSdkConfig(BundleModule repackagedModule) { private Node createCompatConfigXmlNode(Document xmlFactory, BundleModule repackagedModule) { Element compatConfigElement = xmlFactory.createElement(COMPAT_CONFIG_ELEMENT_NAME); appendCompatEntrypointElement(compatConfigElement, xmlFactory); - appendResourcesPackageIdElement(compatConfigElement, xmlFactory); - appendRPackageClassNameElement(compatConfigElement, xmlFactory); + appendResourceIdRemappingElement(compatConfigElement, xmlFactory); appendDexPathsToElement(compatConfigElement, xmlFactory, repackagedModule); - appendJavaResourcePathsToElement(compatConfigElement, xmlFactory, repackagedModule); + appendJavaResourcesRootPathToElement(compatConfigElement, xmlFactory, repackagedModule); return compatConfigElement; } @@ -148,18 +157,28 @@ private void appendCompatEntrypointElement(Element compatConfigElement, Document } } - private void appendResourcesPackageIdElement(Element compatConfigElement, Document xmlFactory) { + private void appendResourceIdRemappingElement(Element compatConfigElement, Document xmlFactory) { + Element resourceIdRemappingElement = + xmlFactory.createElement(RESOURCE_ID_REMAPPING_ELEMENT_NAME); + appendResourcesPackageIdElement(resourceIdRemappingElement, xmlFactory); + appendRPackageClassNameElement(resourceIdRemappingElement, xmlFactory); + compatConfigElement.appendChild(resourceIdRemappingElement); + } + + private void appendResourcesPackageIdElement( + Element resourceIdRemappingElement, Document xmlFactory) { Element resourcesPackageIdElement = xmlFactory.createElement(RESOURCES_PACKAGE_ID_ELEMENT_NAME); resourcesPackageIdElement.setTextContent( Integer.toString(sdkDependencyConfig.getResourcesPackageId())); - compatConfigElement.appendChild(resourcesPackageIdElement); + resourceIdRemappingElement.appendChild(resourcesPackageIdElement); } - private void appendRPackageClassNameElement(Element compatConfigElement, Document xmlFactory) { + private void appendRPackageClassNameElement( + Element resourceIdRemappingElement, Document xmlFactory) { Element rPackageClassNameElement = xmlFactory.createElement(R_PACKAGE_CLASS_NAME_ELEMENT_NAME); rPackageClassNameElement.setTextContent( sdkModulesConfig.getSdkPackageName() + "." + R_PACKAGE_CLASS_NAME); - compatConfigElement.appendChild(rPackageClassNameElement); + resourceIdRemappingElement.appendChild(rPackageClassNameElement); } private void appendDexPathsToElement( @@ -178,24 +197,26 @@ private void appendDexPathsToElement( .forEach(compatConfigElement::appendChild); } - private void appendJavaResourcePathsToElement( + private void appendJavaResourcesRootPathToElement( Element compatConfigElement, Document xmlFactory, BundleModule repackagedModule) { - repackagedModule.getEntries().stream() + // Only add java resources root directory to compat config if the module contains java + // resources. + if (getRepackagedJavaResourcesCount(repackagedModule) > 0) { + Element javaResourcesRootPathElement = + xmlFactory.createElement(JAVA_RESOURCES_ROOT_PATH_ELEMENT_NAME); + javaResourcesRootPathElement.setTextContent( + javaResourceRepackager.getNewJavaResourceDirectoryPathInsideAssets()); + compatConfigElement.appendChild(javaResourcesRootPathElement); + } + } + + private long getRepackagedJavaResourcesCount(BundleModule repackagedModule) { + return repackagedModule.getEntries().stream() .filter( entry -> entry .getPath() .startsWith(javaResourceRepackager.getNewJavaResourceDirectoryPath())) - .map( - entry -> { - Element javaResourcePathElement = - xmlFactory.createElement(JAVA_RESOURCE_PATH_ELEMENT_NAME); - javaResourcePathElement.setTextContent( - javaResourceRepackager.getNewJavaResourceDirectoryPathInsideAssets() - + "/" - + entry.getPath().getFileName().toString()); - return javaResourcePathElement; - }) - .forEach(compatConfigElement::appendChild); + .count(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java b/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java index 243b9035..a815fef7 100644 --- a/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java +++ b/src/main/java/com/android/tools/build/bundletool/shards/ModuleSplitterForShards.java @@ -47,7 +47,7 @@ public class ModuleSplitterForShards { private static final ImmutableSet SUFFIX_STRIPPING_DIMENSIONS = - ImmutableSet.of(Value.TEXTURE_COMPRESSION_FORMAT, Value.DEVICE_TIER); + ImmutableSet.of(Value.TEXTURE_COMPRESSION_FORMAT, Value.DEVICE_TIER, Value.COUNTRY_SET); private final Version bundleVersion; private final BundleConfig bundleConfig; diff --git a/src/main/java/com/android/tools/build/bundletool/shards/Sharder.java b/src/main/java/com/android/tools/build/bundletool/shards/Sharder.java index 6f18763b..77cb23de 100644 --- a/src/main/java/com/android/tools/build/bundletool/shards/Sharder.java +++ b/src/main/java/com/android/tools/build/bundletool/shards/Sharder.java @@ -153,9 +153,11 @@ private static ImmutableSet getMasterSplits(ImmutableListSee {@link AssetsDimensionSplitterFactory} for details of the implementation. + */ +public class CountrySetAssetsSplitter { + + /** Creates a {@link ModuleSplitSplitter} capable of splitting assets by country set. */ + public static ModuleSplitSplitter create(boolean stripTargetingSuffix) { + return AssetsDimensionSplitterFactory.createSplitter( + AssetsDirectoryTargeting::getCountrySet, + CountrySetAssetsSplitter::fromCountrySet, + ApkTargeting::hasCountrySetTargeting, + stripTargetingSuffix ? Optional.of(TargetingDimension.COUNTRY_SET) : Optional.empty()); + } + + private static ApkTargeting fromCountrySet(CountrySetTargeting targeting) { + return ApkTargeting.newBuilder().setCountrySetTargeting(targeting).build(); + } + + // Do not instantiate. + private CountrySetAssetsSplitter() {} +} 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 b4d3d1ef..0c40dbbf 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 @@ -473,6 +473,14 @@ private SplittingPipeline createAssetsSplittingPipeline() { apkGenerationConfiguration.shouldStripTargetingSuffix( OptimizationDimension.DEVICE_TIER))); } + if (apkGenerationConfiguration + .getOptimizationDimensions() + .contains(OptimizationDimension.COUNTRY_SET)) { + assetsSplitters.add( + CountrySetAssetsSplitter.create( + apkGenerationConfiguration.shouldStripTargetingSuffix( + OptimizationDimension.COUNTRY_SET))); + } return new SplittingPipeline(assetsSplitters.build()); } diff --git a/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java index 7ee7a022..bbedbc55 100644 --- a/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java +++ b/src/main/java/com/android/tools/build/bundletool/transparency/CodeTransparencyFactory.java @@ -67,7 +67,7 @@ private static CodeRelatedFile createArchivedCodeRelatedFile(AppBundle bundle) { try { ResourceReader resourceReader = new ResourceReader(); ArchivedResourcesHelper archivedResourcesHelper = - new ArchivedResourcesHelper(new ResourceReader()); + new ArchivedResourcesHelper(resourceReader); String resourcePath = archivedResourcesHelper.findArchivedClassesDexPath( bundle.getVersion(), /* transparencyEnabled= */ true); diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java index 12218bb2..6a0e42a7 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java @@ -47,6 +47,7 @@ public class AppBundleValidator { new AbiParityValidator(), new TextureCompressionFormatParityValidator(), new DeviceTierParityValidator(), + new CountrySetParityValidator(), new DexFilesValidator(), new ApexBundleValidator(), new AssetBundleValidator(), diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidator.java index 5f996832..192779ef 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidator.java @@ -91,6 +91,9 @@ private static void checkNoDimensionWithoutValuesAndAlternatives( checkHasValuesOrAlternatives( targeting.getTextureCompressionFormat(), targetedDirectory.getPath()); } + if (targeting.hasCountrySet()) { + checkHasValuesOrAlternatives(targeting.getCountrySet(), targetedDirectory.getPath()); + } } private static void checkNoOverlapInValuesAndAlternatives( @@ -100,5 +103,6 @@ private static void checkNoOverlapInValuesAndAlternatives( checkValuesAndAlternativeHaveNoOverlap(targeting.getLanguage(), targetedDirectory.getPath()); checkValuesAndAlternativeHaveNoOverlap( targeting.getTextureCompressionFormat(), targetedDirectory.getPath()); + checkValuesAndAlternativeHaveNoOverlap(targeting.getCountrySet(), targetedDirectory.getPath()); } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java index 45d2b75f..74d2a4cc 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleConfigValidator.java @@ -49,7 +49,7 @@ public final class BundleConfigValidator extends SubValidator { private static final ImmutableSet FORBIDDEN_CHARS_IN_GLOB = ImmutableSet.of("\n", "\\\\"); private static final ImmutableSet SUFFIX_STRIPPING_ENABLED_DIMENSIONS = - ImmutableSet.of(Value.TEXTURE_COMPRESSION_FORMAT, Value.DEVICE_TIER); + ImmutableSet.of(Value.TEXTURE_COMPRESSION_FORMAT, Value.DEVICE_TIER, Value.COUNTRY_SET); @Override public void validateBundle(AppBundle bundle) { diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java index 18c8fad1..84b3bc0c 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleModulesValidator.java @@ -55,6 +55,7 @@ public class BundleModulesValidator { new AbiParityValidator(), new TextureCompressionFormatParityValidator(), new DeviceTierParityValidator(), + new CountrySetParityValidator(), new DexFilesValidator(), new ApexBundleValidator(), new AssetBundleValidator(), diff --git a/src/main/java/com/android/tools/build/bundletool/validation/BundleValidationUtils.java b/src/main/java/com/android/tools/build/bundletool/validation/BundleValidationUtils.java index c63dab02..0f84291d 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/BundleValidationUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/BundleValidationUtils.java @@ -23,6 +23,7 @@ import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.TextureCompressionFormat; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; @@ -66,6 +67,15 @@ public static void checkHasValuesOrAlternatives( } } + public static void checkHasValuesOrAlternatives( + CountrySetTargeting targeting, String directoryPath) { + if (targeting.getValueCount() == 0 && targeting.getAlternativesCount() == 0) { + throw InvalidBundleException.builder() + .withUserMessage("Directory '%s' has set but empty Country Set targeting.", directoryPath) + .build(); + } + } + public static void checkValuesAndAlternativeHaveNoOverlap( AbiTargeting targeting, String directoryPath) { SetView intersection = @@ -118,6 +128,22 @@ public static void checkValuesAndAlternativeHaveNoOverlap( } } + public static void checkValuesAndAlternativeHaveNoOverlap( + CountrySetTargeting targeting, String directoryPath) { + SetView intersection = + Sets.intersection( + ImmutableSet.copyOf(targeting.getValueList()), + ImmutableSet.copyOf(targeting.getAlternativesList())); + if (!intersection.isEmpty()) { + throw InvalidBundleException.builder() + .withUserMessage( + "Expected targeting values and alternatives to be mutually exclusive, but directory" + + " '%s' has country set targeting that contains %s in both.", + directoryPath, intersection) + .build(); + } + } + /** Checks whether directory inside the specified module contains any files. */ public static boolean directoryContainsNoFiles(BundleModule module, ZipPath dir) { return module.findEntriesUnderPath(dir).count() == 0; 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 new file mode 100644 index 00000000..a94f945e --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/CountrySetParityValidator.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2022 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.validation; + +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.extractAssetsTargetedDirectories; +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.extractCountrySets; +import static com.google.common.collect.MoreCollectors.toOptional; + +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.Config.Optimizations; +import com.android.bundle.Config.SplitDimension; +import com.android.bundle.Config.SplitDimension.Value; +import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.targeting.TargetedDirectory; +import com.android.tools.build.bundletool.model.targeting.TargetingDimension; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import java.util.List; +import java.util.Optional; + +/** Validates that all modules that use country set targeting contain the same country sets. */ +public class CountrySetParityValidator extends SubValidator { + + /** + * Represents a set of country sets (with fallback presence) in a module or a targeted asset + * directory. + */ + @AutoValue + public abstract static class SupportedCountrySets { + public static CountrySetParityValidator.SupportedCountrySets create( + ImmutableSet countrySets, boolean hasFallback) { + return new AutoValue_CountrySetParityValidator_SupportedCountrySets(countrySets, hasFallback); + } + + public abstract ImmutableSet getCountrySets(); + + public abstract boolean getHasFallback(); + + @Override + public final String toString() { + return getCountrySets() + + (getHasFallback() ? " (with fallback directories)" : " (without fallback directories)"); + } + } + + @Override + public void validateBundle(AppBundle bundle) { + BundleConfig bundleConfig = bundle.getBundleConfig(); + Optimizations optimizations = bundleConfig.getOptimizations(); + List splitDimensions = optimizations.getSplitsConfig().getSplitDimensionList(); + + Optional countrySetDefaultSuffix = + splitDimensions.stream() + .filter(dimension -> dimension.getValue().equals(Value.COUNTRY_SET)) + .map(dimension -> dimension.getSuffixStripping().getDefaultSuffix()) + .collect(toOptional()); + + if (countrySetDefaultSuffix.isPresent()) { + validateDefaultCountrySetSupportedByAllModules(bundle, countrySetDefaultSuffix.get()); + } + } + + @Override + public void validateAllModules(ImmutableList modules) { + BundleModule referenceModule = null; + SupportedCountrySets referenceCountrySets = null; + for (BundleModule module : modules) { + SupportedCountrySets moduleCountrySets = getSupportedCountrySets(module); + if (moduleCountrySets.getCountrySets().isEmpty()) { + continue; + } + + if (referenceCountrySets == null) { + referenceModule = module; + referenceCountrySets = moduleCountrySets; + } else if (!referenceCountrySets.equals(moduleCountrySets)) { + throw InvalidBundleException.builder() + .withUserMessage( + "All modules with country set targeting must support the same country" + + " sets, but module '%s' supports %s and module '%s' supports %s.", + referenceModule.getName(), + referenceCountrySets, + module.getName(), + moduleCountrySets) + .build(); + } + } + } + + private void validateDefaultCountrySetSupportedByAllModules( + AppBundle bundle, String defaultCountrySet) { + + bundle + .getModules() + .values() + .forEach( + module -> { + SupportedCountrySets supportedCountrySets = getSupportedCountrySets(module); + if (supportedCountrySets.getCountrySets().isEmpty()) { + return; + } + + if (!defaultCountrySet.isEmpty()) { + if (!supportedCountrySets.getCountrySets().contains(defaultCountrySet)) { + throw InvalidBundleException.builder() + .withUserMessage( + "When a standalone or universal APK is built, the country set" + + " folders corresponding to country set '%s' 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.", + 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(); + } + } + }); + } + + private SupportedCountrySets getSupportedCountrySets(BundleModule module) { + // Extract targeted directories from entries (like done when generating assets targeting) + ImmutableSet targetedDirectories = extractAssetsTargetedDirectories(module); + + // Inspect the targetings to extract country sets. + ImmutableSet countrySets = extractCountrySets(targetedDirectories); + + // Check if one or more targeted directories have "fallback" sibling directories. + boolean hasFallback = + targetedDirectories.stream() + .anyMatch( + directory -> { + Optional targeting = + directory.getTargeting(TargetingDimension.COUNTRY_SET); + if (targeting.isPresent()) { + // Check if a sibling folder without country set targeting exists. If yes, this + // is called a "fallback". + TargetedDirectory siblingFallbackDirectory = + directory.removeTargeting(TargetingDimension.COUNTRY_SET); + return module + .findEntriesUnderPath(siblingFallbackDirectory.toZipPath()) + .findAny() + .isPresent(); + } + + return false; + }); + + return SupportedCountrySets.create(countrySets, hasFallback); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidator.java index 504463b9..b66bf2cc 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidator.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.validation; +import static com.android.tools.build.bundletool.model.utils.CollectorUtils.groupingByDeterministic; import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static java.util.Collections.max; @@ -26,29 +27,133 @@ import com.android.bundle.DeviceTier; import com.android.bundle.DeviceTierConfig; import com.android.bundle.DeviceTierSet; +import com.android.bundle.UserCountrySet; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import java.util.List; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** Validates the contents of a DeviceTierConfig. */ public class DeviceTierConfigValidator { + private static final String COUNTRY_SET_NAME_REGEX_STRING = "^[a-zA-Z][a-zA-Z0-9_]*$"; + private static final String COUNTRY_CODE_REGEX_STRING = "^[A-Z]{2}$"; + private static final Pattern COUNTRY_SET_NAME_REGEX = + Pattern.compile(COUNTRY_SET_NAME_REGEX_STRING); + private static final Pattern COUNTRY_CODE_REGEX = Pattern.compile(COUNTRY_CODE_REGEX_STRING); + private DeviceTierConfigValidator() {} - /** Validates the DeviceGroups and DeviceTierSet of a DeviceTierConfig. */ + /** Validates the DeviceGroups, DeviceTierSet and UserCountrySets of a DeviceTierConfig. */ public static void validateDeviceTierConfig(DeviceTierConfig deviceTierConfig) { + if (deviceTierConfig.getUserCountrySetsList().isEmpty() + && deviceTierConfig.getDeviceGroupsList().isEmpty()) { + throw CommandExecutionException.builder() + .withInternalMessage( + "The device tier config must contain at least one group or user country set.") + .build(); + } + + validateUserCountrySets(deviceTierConfig.getUserCountrySetsList()); validateGroups(deviceTierConfig.getDeviceGroupsList()); validateTiers(deviceTierConfig.getDeviceTierSet(), deviceTierConfig.getDeviceGroupsList()); } - private static void validateGroups(List deviceGroups) { - if (deviceGroups.isEmpty()) { + /** + * Validates the given country code. Checks that the given country code matches the regex + * '^[A-Z]{2}$'. + */ + public static void validateCountryCode(String countryCode) { + if (!COUNTRY_CODE_REGEX.matcher(countryCode).matches()) { + throw CommandExecutionException.builder() + .withInternalMessage( + "Country code should match the regex '%s', but found '%s'.", + COUNTRY_CODE_REGEX, countryCode) + .build(); + } + } + + private static void validateUserCountrySets(List userCountrySets) { + userCountrySets.forEach(DeviceTierConfigValidator::validateUserCountrySet); + validateCountrySetNamesAreUnique(userCountrySets); + validateCountryCodesAreUnique(userCountrySets); + } + + private static void validateCountrySetNamesAreUnique(List userCountrySets) { + ImmutableSet duplicateNames = + userCountrySets.stream() + .map(UserCountrySet::getName) + .collect(groupingByDeterministic(Function.identity(), Collectors.counting())) + .entrySet() + .stream() + .filter(e -> e.getValue() > 1) + .map(e -> e.getKey()) + .collect(toImmutableSet()); + + if (!duplicateNames.isEmpty()) { + throw CommandExecutionException.builder() + .withInternalMessage( + "Country set names should be unique. Found multiple country sets with these names:" + + " %s.", + duplicateNames) + .build(); + } + } + + private static void validateCountryCodesAreUnique(List userCountrySets) { + ImmutableSet duplicateCountryCodes = + userCountrySets.stream() + .flatMap(userCountrySet -> userCountrySet.getCountryCodesList().stream()) + .collect(groupingByDeterministic(Function.identity(), Collectors.counting())) + .entrySet() + .stream() + .filter(e -> e.getValue() > 1) + .map(e -> e.getKey()) + .collect(toImmutableSet()); + + if (!duplicateCountryCodes.isEmpty()) { throw CommandExecutionException.builder() - .withInternalMessage("The device tier config must contain at least one group.") + .withInternalMessage( + "A country code can belong to only one country set. Found multiple occurrences of" + + " these country codes: %s.", + duplicateCountryCodes.toString()) .build(); } + } + + private static void validateUserCountrySet(UserCountrySet userCountrySet) { + validateCountrySetName(userCountrySet.getName()); + if (userCountrySet.getCountryCodesList().isEmpty()) { + throw CommandExecutionException.builder() + .withInternalMessage( + "Country set '%s' must specify at least one country code.", userCountrySet.getName()) + .build(); + } + + userCountrySet.getCountryCodesList().forEach(DeviceTierConfigValidator::validateCountryCode); + } + + private static void validateCountrySetName(String countrySetName) { + if (countrySetName.isEmpty()) { + throw CommandExecutionException.builder() + .withInternalMessage("Country Sets must specify a name.") + .build(); + } + + if (!COUNTRY_SET_NAME_REGEX.matcher(countrySetName).matches()) { + throw CommandExecutionException.builder() + .withInternalMessage( + "Country set name should match the regex '%s', but found '%s'.", + COUNTRY_SET_NAME_REGEX_STRING, countrySetName) + .build(); + } + } + + private static void validateGroups(List deviceGroups) { for (DeviceGroup deviceGroup : deviceGroups) { if (deviceGroup.getName().isEmpty()) { throw CommandExecutionException.builder() diff --git a/src/main/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidator.java index df1560bc..4d63001d 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidator.java @@ -69,7 +69,7 @@ public static SupportedTextureCompressionFormats create( @Override public final String toString() { - return getFormats().toString() + return getFormats() + (getHasFallback() ? " (with fallback directories)" : " (without fallback directories)"); } } diff --git a/src/main/proto/config.proto b/src/main/proto/config.proto index be2eab08..64e60901 100644 --- a/src/main/proto/config.proto +++ b/src/main/proto/config.proto @@ -249,6 +249,7 @@ message SplitDimension { LANGUAGE = 3; TEXTURE_COMPRESSION_FORMAT = 4; DEVICE_TIER = 6; + COUNTRY_SET = 7; } Value value = 1; diff --git a/src/main/proto/devices.proto b/src/main/proto/devices.proto index e72876b4..252e95e9 100644 --- a/src/main/proto/devices.proto +++ b/src/main/proto/devices.proto @@ -38,6 +38,9 @@ message DeviceSpec { // Information about the runtime-enabled SDK capabilities of the device. SdkRuntime sdk_runtime = 10; + + // Country set. + google.protobuf.StringValue country_set = 11; } message SdkRuntime { diff --git a/src/main/proto/targeting.proto b/src/main/proto/targeting.proto index 8614bc3d..888eb01a 100644 --- a/src/main/proto/targeting.proto +++ b/src/main/proto/targeting.proto @@ -27,6 +27,7 @@ message ApkTargeting { MultiAbiTargeting multi_abi_targeting = 7; SanitizerTargeting sanitizer_targeting = 8; DeviceTierTargeting device_tier_targeting = 9; + CountrySetTargeting country_set_targeting = 10; } // Targeting on the module level. @@ -130,6 +131,7 @@ message AssetsDirectoryTargeting { TextureCompressionFormatTargeting texture_compression_format = 3; LanguageTargeting language = 4; DeviceTierTargeting device_tier = 5; + CountrySetTargeting country_set = 6; } // Targeting specific for directories under lib/. @@ -207,6 +209,31 @@ message DeviceTierTargeting { reserved 1, 2; } +// Targets assets and APKs to a specific country set. +// For Example:- +// The values and alternatives for the following files in assets directory +// targeting would be as follows: +// assetpack1/assets/foo#countries_latam/bar.txt -> +// { value: [latam], alternatives: [sea] } +// assetpack1/assets/foo#countries_sea/bar.txt -> +// { value: [sea], alternatives: [latam] } +// assetpack1/assets/foo/bar.txt -> +// { value: [], alternatives: [sea, latam] } +// The values and alternatives for the following targeted split apks would be as +// follows: +// splits/base-countries_latam.apk -> +// { value: [latam], alternatives: [sea] } +// splits/base-countries_sea.apk -> +// { value: [sea], alternatives: [latam] } +// splits/base-other_countries.apk -> +// { value: [], alternatives: [sea, latam] } +message CountrySetTargeting { + // Country set name defined in device tier config. + repeated string value = 1; + // Targeting of other sibling directories that were in the Bundle. + repeated string alternatives = 2; +} + // Targets conditional modules to a set of device groups. message DeviceGroupModuleTargeting { repeated string value = 1; diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java index 623f17fb..fa14b4bb 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java @@ -685,6 +685,71 @@ public void buildingViaFlagsAndBuilderHasSameResult_optionalDeviceTier() throws assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); } + @Test + public void buildingViaFlagsAndBuilderHasSameResult_optionalCountrySet() throws Exception { + Path deviceSpecPath = + createDeviceSpecFile( + mergeSpecs(sdkVersion(28), density(DensityAlias.HDPI), abis("x86"), locales("en")), + tmpDir.resolve("device.json")); + final String countrySet = "latam"; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BuildApksCommand commandViaFlags = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--aapt2=" + AAPT2_PATH, + // Optional values. + "--device-spec=" + deviceSpecPath, + "--country-set=" + countrySet), + new PrintStream(output), + systemEnvironmentProvider, + fakeAdbServer); + BuildApksCommand.Builder commandViaBuilder = + BuildApksCommand.builder() + .setBundlePath(bundlePath) + .setOutputFile(outputFilePath) + // Optional values. + .setDeviceSpec(deviceSpecPath) + .setCountrySet(countrySet) + // Must copy instance of the internal executor service. + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setExecutorServiceInternal(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true) + .setOutputPrintStream(commandViaFlags.getOutputPrintStream().get()); + DebugKeystoreUtils.getDebugSigningConfiguration(systemEnvironmentProvider) + .ifPresent(commandViaBuilder::setSigningConfiguration); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + + @Test + public void countrySetWithoutDeviceSpecOrConnectedDevice_throws() throws Exception { + final String countrySet = "latam"; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + InvalidCommandException exception = + assertThrows( + InvalidCommandException.class, + () -> + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--aapt2=" + AAPT2_PATH, + // Optional values. + "--country-set=" + countrySet), + new PrintStream(output), + systemEnvironmentProvider, + fakeAdbServer)); + assertThat(exception) + .hasMessageThat() + .contains( + "Setting --country-set requires using either the --connected-device or the" + + " --device-spec flag."); + } + @Test public void buildingViaFlagsAndBuilderHasSameResult_7zipPath() throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java index 12b34007..aa9e6ae9 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksManagerTest.java @@ -34,6 +34,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.stream; import com.android.apksig.ApkVerifier; @@ -52,6 +53,7 @@ import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder; import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.utils.ZipUtils; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.CertificateFactory; import com.android.tools.build.bundletool.testing.SdkBundleBuilder; @@ -271,10 +273,17 @@ public void sdkManifestMutation_compatSdkProviderClassNameSet() throws Exception ApkDescription apkDescription = variant.getApkSet(0).getApkDescription(0); File apkFile = extractFromApkSetFile(apkSetFile, apkDescription.getPath(), tmpDir); AndroidManifest manifest = extractAndroidManifest(apkFile, tmpDir); + ZipFile apkZip = new ZipFile(apkFile); assertThat(manifest.getSdkProviderClassNameProperty()).hasValue(sdkProviderClassName); assertThat(manifest.getCompatSdkProviderClassNameProperty()) .hasValue(compatSdkProviderClassName); + String compatSdkProviderClassNameFromFile = + ZipUtils.asByteSource( + apkZip, apkZip.getEntry("assets/SandboxedSdkProviderCompatClassName.txt")) + .asCharSource(UTF_8) + .read(); + assertThat(compatSdkProviderClassNameFromFile).isEqualTo(compatSdkProviderClassName); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommandTest.java index d7b1ecfa..2344e538 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/EvaluateDeviceTargetingConfigCommandTest.java @@ -25,6 +25,7 @@ import com.android.tools.build.bundletool.device.AdbServer; import com.android.tools.build.bundletool.flags.Flag.RequiredFlagNotSetException; import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.flags.ParsedFlags; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.google.common.io.CharStreams; @@ -46,6 +47,7 @@ public class EvaluateDeviceTargetingConfigCommandTest { @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private static final String DEVICE_ID = "id"; + private static final String COUNTRY_CODE = "AR"; private Path deviceTargetingConfigPath; private Path devicePropertiesPath; private static final Path ADB_PATH = @@ -66,13 +68,15 @@ public void buildingViaFlagsAndBuilderHasSameResult_withDeviceProperties() { new FlagParser() .parse( "--config=" + deviceTargetingConfigPath, - "--device-properties=" + devicePropertiesPath), + "--device-properties=" + devicePropertiesPath, + "--country-code=" + COUNTRY_CODE), fakeAdbServer); EvaluateDeviceTargetingConfigCommand commandViaBuilder = EvaluateDeviceTargetingConfigCommand.builder() .setDeviceTargetingConfigurationPath(deviceTargetingConfigPath) .setDevicePropertiesPath(devicePropertiesPath) + .setCountryCode(COUNTRY_CODE) .build(); assertThat(commandViaBuilder).isEqualTo(commandViaFlags); @@ -87,6 +91,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_withConnectedDevice() { "--config=" + deviceTargetingConfigPath, "--connected-device", "--device-id=" + DEVICE_ID, + "--country-code=" + COUNTRY_CODE, "--adb=" + ADB_PATH), fakeAdbServer); @@ -96,6 +101,7 @@ public void buildingViaFlagsAndBuilderHasSameResult_withConnectedDevice() { .setAdbPath(ADB_PATH) .setConnectedDeviceMode(true) .setDeviceId(Optional.of(DEVICE_ID)) + .setCountryCode(COUNTRY_CODE) .setAdbServer(fakeAdbServer) .build(); @@ -217,6 +223,34 @@ public void defaultTierSelectedAndNoGroups() throws Exception { "no_groups_default_tier_evaluation.txt"); } + @Test + public void countrySetWithMultipleGroupsAndTiers() throws Exception { + assertOutputIsExpected( + "country_sets_with_multiple_groups_and_tiers.json", + "very_high_ram_device_properties.json", + Optional.of(COUNTRY_CODE), + "country_sets_with_multiple_groups_and_tiers_evaluation.txt"); + } + + @Test + public void countrySetWithoutGroupsAndTiers() throws Exception { + assertOutputIsExpected( + "only_country_sets.json", + "very_high_ram_device_properties.json", + Optional.of(COUNTRY_CODE), + "only_country_sets_evaluation.txt"); + } + + @Test + public void countrySetWithoutGroupsAndTiers_countryCodeNotPresentInDefinedCountrySets() + throws Exception { + assertOutputIsExpected( + "only_country_sets.json", + "very_high_ram_device_properties.json", + Optional.of("UK"), + "only_country_sets_restofworld_country_code_evaluation.txt"); + } + @Test public void deviceTierConfigValidatorIsCalled() throws Exception { EvaluateDeviceTargetingConfigCommand command = @@ -244,14 +278,26 @@ public void deviceTierConfigValidatorIsCalled() throws Exception { private void assertOutputIsExpected( String configFileName, String devicePropertiesFileName, String expectedOutputFileName) throws Exception { + assertOutputIsExpected( + configFileName, + devicePropertiesFileName, + /* countryCode =*/ Optional.empty(), + expectedOutputFileName); + } + + private void assertOutputIsExpected( + String configFileName, + String devicePropertiesFileName, + Optional countryCode, + String expectedOutputFileName) + throws Exception { String testFilePath = "testdata/device_targeting_config/"; EvaluateDeviceTargetingConfigCommand command = EvaluateDeviceTargetingConfigCommand.fromFlags( - new FlagParser() - .parse( - "--config=" + TestData.copyToTempDir(tmp, testFilePath + configFileName), - "--device-properties=" - + TestData.copyToTempDir(tmp, testFilePath + devicePropertiesFileName)), + getParsedFlags( + TestData.copyToTempDir(tmp, testFilePath + configFileName).toString(), + TestData.copyToTempDir(tmp, testFilePath + devicePropertiesFileName).toString(), + countryCode), fakeAdbServer); try (ByteArrayOutputStream outputByteArrayStream = new ByteArrayOutputStream(); @@ -265,4 +311,17 @@ private void assertOutputIsExpected( assertThat(actualOutput).isEqualTo(expectedOutput); } } + + private ParsedFlags getParsedFlags( + String deviceTierConfigPath, String devicePropertiesPath, Optional countryCode) { + if (countryCode.isPresent()) { + return new FlagParser() + .parse( + "--config=" + deviceTierConfigPath, + "--device-properties=" + devicePropertiesPath, + "--country-code=" + countryCode.get()); + } + return new FlagParser() + .parse("--config=" + deviceTierConfigPath, "--device-properties=" + devicePropertiesPath); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java index fd37dfec..be8c3fb4 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java @@ -45,10 +45,13 @@ import static com.android.tools.build.bundletool.testing.DeviceFactory.mergeSpecs; import static com.android.tools.build.bundletool.testing.DeviceFactory.sdkRuntimeSupported; import static com.android.tools.build.bundletool.testing.DeviceFactory.sdkVersion; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDeviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeModuleTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.moduleFeatureTargeting; @@ -103,6 +106,7 @@ import com.google.common.collect.Iterables; import com.google.common.io.MoreFiles; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import com.google.protobuf.util.JsonFormat; import java.io.ByteArrayOutputStream; import java.io.PrintStream; @@ -1950,6 +1954,214 @@ public void bundleWithDeviceTierTargeting_deviceTierSet_filtersByTier() throws E } } + @Test + public void bundleWithCountrySetTargeting_noCountrySetSpecifiedNorDefault_usesFallback() + throws Exception { + ZipPath baseMasterApk = ZipPath.create("base-master.apk"); + ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); + ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); + ZipPath baseLatamApk = ZipPath.create("base-countries_latam.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), baseMasterApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", + /* alternatives= */ ImmutableList.of("latam"))), + baseSeaApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", + /* alternatives= */ ImmutableList.of("sea"))), + baseLatamApk), + splitApkDescription( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("sea", "latam"))), + baseRestOfWorldApk)))) + .addDefaultTargetingValue( + DefaultTargetingValue.newBuilder().setDimension(Value.COUNTRY_SET)) + .build(); + Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + DeviceSpec deviceSpec = lDevice(); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly(inOutputDirectory(baseMasterApk), inOutputDirectory(baseRestOfWorldApk)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + + @Test + public void bundleWithCountrySetTargeting_noCountrySetSpecified_usesDefaults() throws Exception { + ZipPath baseMasterApk = ZipPath.create("base-master.apk"); + ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); + ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); + ZipPath baseLatamApk = ZipPath.create("base-countries_latam.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), baseMasterApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", + /* alternatives= */ ImmutableList.of("latam"))), + baseSeaApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", + /* alternatives= */ ImmutableList.of("sea"))), + baseLatamApk), + splitApkDescription( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("sea", "latam"))), + baseRestOfWorldApk)))) + .addDefaultTargetingValue( + DefaultTargetingValue.newBuilder() + .setDimension(Value.COUNTRY_SET) + .setDefaultValue("latam")) + .build(); + Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + DeviceSpec deviceSpec = lDevice(); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly(inOutputDirectory(baseMasterApk), inOutputDirectory(baseLatamApk)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + + @Test + public void bundleWithCountrySetTargeting_countrySetSpecified_filterByCountrySet() + throws Exception { + ZipPath baseMasterApk = ZipPath.create("base-master.apk"); + ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); + ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); + ZipPath baseLatamApk = ZipPath.create("base-countries_latam.apk"); + ZipPath asset1MasterApk = ZipPath.create("asset1-master.apk"); + ZipPath asset1RestOfWorldApk = ZipPath.create("asset1-other_countries.apk"); + ZipPath asset1SeaApk = ZipPath.create("asset1-countries_sea.apk"); + ZipPath asset1LatamApk = ZipPath.create("asset1-countries_latam.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), baseMasterApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", + /* alternatives= */ ImmutableList.of("latam"))), + baseSeaApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", + /* alternatives= */ ImmutableList.of("sea"))), + baseLatamApk), + splitApkDescription( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("sea", "latam"))), + baseRestOfWorldApk)))) + .addAssetSliceSet( + AssetSliceSet.newBuilder() + .setAssetModuleMetadata( + AssetModuleMetadata.newBuilder() + .setName("asset1") + .setDeliveryType(DeliveryType.INSTALL_TIME)) + .addApkDescription( + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), asset1MasterApk)) + .addApkDescription( + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", + /* alternatives= */ ImmutableList.of("sea"))), + asset1LatamApk)) + .addApkDescription( + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", + /* alternatives= */ ImmutableList.of("latam"))), + asset1SeaApk)) + .addApkDescription( + splitApkDescription( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea"))), + asset1RestOfWorldApk))) + .addDefaultTargetingValue( + DefaultTargetingValue.newBuilder().setDimension(Value.COUNTRY_SET)) + .build(); + Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + DeviceSpec deviceSpec = lDevice().toBuilder().setCountrySet(StringValue.of("latam")).build(); + + ImmutableList matchedApks = + ExtractApksCommand.builder() + .setApksArchivePath(apksArchiveFile) + .setDeviceSpec(deviceSpec) + .setOutputDirectory(tmpDir) + .build() + .execute(); + + assertThat(matchedApks) + .containsExactly( + inOutputDirectory(baseMasterApk), + inOutputDirectory(baseLatamApk), + inOutputDirectory(asset1MasterApk), + inOutputDirectory(asset1LatamApk)); + for (Path matchedApk : matchedApks) { + checkFileExistsAndReadable(tmpDir.resolve(matchedApk)); + } + } + @Test public void incompleteApksFile_missingMatchedAbiSplit_throws() throws Exception { // Partial APK Set file where 'x86' split is included and 'x86_64' split is not included because diff --git a/src/test/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommandTest.java index fbe9ab5f..0dd71391 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/GetDeviceSpecCommandTest.java @@ -67,6 +67,7 @@ public class GetDeviceSpecCommandTest { private static final int DEVICE_TIER = 1; private static final ImmutableSet DEVICE_GROUPS = ImmutableSet.of("highRam", "googlePixel"); + private static final String COUNTRY_SET = "latam"; private SystemEnvironmentProvider systemEnvironmentProvider; private Path adbPath; @@ -145,7 +146,8 @@ public void fromFlagsEquivalentToBuilder_allFlagsUsed() { "--device-id=" + DEVICE_ID, "--output=" + outputPath, "--device-tier=" + DEVICE_TIER, - "--device-groups=" + Joiner.on(",").join(DEVICE_GROUPS)), + "--device-groups=" + Joiner.on(",").join(DEVICE_GROUPS), + "--country-set=" + COUNTRY_SET), systemEnvironmentProvider, fakeServerOneDevice(lDeviceWithLocales("en-US"))); @@ -157,6 +159,7 @@ public void fromFlagsEquivalentToBuilder_allFlagsUsed() { .setAdbServer(commandViaFlags.getAdbServer()) .setDeviceTier(DEVICE_TIER) .setDeviceGroups(DEVICE_GROUPS) + .setCountrySet(COUNTRY_SET) .build(); assertThat(commandViaFlags).isEqualTo(commandViaBuilder); @@ -356,16 +359,6 @@ public void printHelp_doesNotCrash() { GetDeviceSpecCommand.help(); } - private static AdbServer fakeServerOneDevice(DeviceSpec deviceSpec) { - return new FakeAdbServer( - /* hasInitialDeviceList= */ true, - ImmutableList.of(FakeDevice.fromDeviceSpec("id", DeviceState.ONLINE, deviceSpec))); - } - - private static AdbServer fakeServerNoDevices() { - return new FakeAdbServer(/* hasInitialDeviceList= */ true, /* devices= */ ImmutableList.of()); - } - @Test public void overwriteSet_overwritesFile() throws Exception { DeviceSpec deviceSpec = mergeSpecs(sdkVersion(21), density(480), abis("x86"), locales("en-US")); @@ -398,4 +391,68 @@ public void overwriteNotSet_outputFileExists_throws() throws Exception { Throwable exception = assertThrows(IllegalArgumentException.class, () -> command.execute()); assertThat(exception).hasMessageThat().contains("File '" + outputPath + "' already exists."); } + + @Test + public void countrySet_setInDeviceSpec_whenSpecified() throws Exception { + // Arrange + DeviceSpec deviceSpec = mergeSpecs(sdkVersion(21), density(480), abis("x86"), locales("en-US")); + Path outputPath = tmp.getRoot().toPath().resolve("device.json"); + Files.createFile(outputPath); + GetDeviceSpecCommand command = + GetDeviceSpecCommand.builder() + .setAdbPath(adbPath) + .setAdbServer(fakeServerOneDevice(deviceSpec)) + .setOutputPath(outputPath) + .setCountrySet("latam") + .setOverwriteOutput(true) + .build(); + + // Act + command.execute(); + + // Assert + try (Reader deviceSpecReader = BufferedIo.reader(outputPath)) { + DeviceSpec.Builder writtenSpecBuilder = DeviceSpec.newBuilder(); + JsonFormat.parser().merge(deviceSpecReader, writtenSpecBuilder); + DeviceSpec writtenSpec = writtenSpecBuilder.build(); + assertThat(writtenSpec.hasCountrySet()).isTrue(); + assertThat(writtenSpec.getCountrySet().getValue()).isEqualTo("latam"); + } + } + + @Test + public void countrySet_notSetInDeviceSpec_whenNotSpecified() throws Exception { + // Arrange + DeviceSpec deviceSpec = mergeSpecs(sdkVersion(21), density(480), abis("x86"), locales("en-US")); + Path outputPath = tmp.getRoot().toPath().resolve("device.json"); + Files.createFile(outputPath); + GetDeviceSpecCommand command = + GetDeviceSpecCommand.builder() + .setAdbPath(adbPath) + .setAdbServer(fakeServerOneDevice(deviceSpec)) + .setOutputPath(outputPath) + .setOverwriteOutput(true) + .build(); + + // Act + command.execute(); + + // Assert + try (Reader deviceSpecReader = BufferedIo.reader(outputPath)) { + DeviceSpec.Builder writtenSpecBuilder = DeviceSpec.newBuilder(); + JsonFormat.parser().merge(deviceSpecReader, writtenSpecBuilder); + DeviceSpec writtenSpec = writtenSpecBuilder.build(); + assertThat(writtenSpec.hasCountrySet()).isFalse(); + } + } + + private static AdbServer fakeServerOneDevice(DeviceSpec deviceSpec) { + return new FakeAdbServer( + /* hasInitialDeviceList= */ true, + ImmutableList.of(FakeDevice.fromDeviceSpec("id", DeviceState.ONLINE, deviceSpec))); + } + + private static AdbServer fakeServerNoDevices() { + return new FakeAdbServer(/* hasInitialDeviceList= */ true, /* devices= */ ImmutableList.of()); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java index 0aff8a97..9a9a7e98 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/GetSizeCommandTest.java @@ -45,11 +45,13 @@ import static com.android.tools.build.bundletool.testing.DeviceFactory.createDeviceSpecFile; import static com.android.tools.build.bundletool.testing.DeviceFactory.deviceWithSdk; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDeviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkSdkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkTextureTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; @@ -750,37 +752,19 @@ public void getSizeTotalInternal_multipleDimensions() throws Exception { assertThat(configurationSizes.getMinSizeConfigurationMap()) .containsExactly( - SizeConfiguration.builder() - .setSdkVersion("21-") - .setScreenDensity("LDPI") - .build(), + SizeConfiguration.builder().setSdkVersion("21-").setScreenDensity("LDPI").build(), 2 * compressedApkSize, - SizeConfiguration.builder() - .setSdkVersion("21-") - .setScreenDensity("MDPI") - .build(), + SizeConfiguration.builder().setSdkVersion("21-").setScreenDensity("MDPI").build(), 2 * compressedApkSize, - SizeConfiguration.builder() - .setSdkVersion("15-20") - .setAbi("armeabi") - .build(), + SizeConfiguration.builder().setSdkVersion("15-20").setAbi("armeabi").build(), compressedApkSize); assertThat(configurationSizes.getMaxSizeConfigurationMap()) .containsExactly( - SizeConfiguration.builder() - .setSdkVersion("21-") - .setScreenDensity("LDPI") - .build(), + SizeConfiguration.builder().setSdkVersion("21-").setScreenDensity("LDPI").build(), 2 * compressedApkSize, - SizeConfiguration.builder() - .setSdkVersion("21-") - .setScreenDensity("MDPI") - .build(), + SizeConfiguration.builder().setSdkVersion("21-").setScreenDensity("MDPI").build(), 2 * compressedApkSize, - SizeConfiguration.builder() - .setSdkVersion("15-20") - .setAbi("armeabi") - .build(), + SizeConfiguration.builder().setSdkVersion("15-20").setAbi("armeabi").build(), compressedApkSize); } @@ -897,7 +881,6 @@ public void getSizeTotal_noDimensions_prettyPrint() throws Exception { + CRLF); } - @Test public void getSizeTotal_withSelectModules() throws Exception { Variant lVariant = @@ -1213,6 +1196,86 @@ public void getSizeTotal_withAssetModules() throws Exception { "21-", "x86", "etc2", 4 * compressedApkSize, 4 * compressedApkSize)); } + @Test + public void getSizeTotal_withAssetModules_selectedModules() throws Exception { + Variant lVariant = + createVariant( + lPlusVariantTargeting(), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), + createApkDescription( + apkAbiTargeting(X86, ImmutableSet.of(X86_64)), + ZipPath.create("base-x86.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkAbiTargeting(X86_64, ImmutableSet.of(X86)), + ZipPath.create("base-x86_64.apk"), + /* isMasterSplit= */ false))); + + AssetSliceSet assetModule = + createAssetSliceSet( + /* moduleName= */ "asset1", + DeliveryType.INSTALL_TIME, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("asset1-master.apk")), + createApkDescription( + apkTextureTargeting(ETC2, ImmutableSet.of(ASTC)), + ZipPath.create("asset1-tcf_etc2.apk"), + /* isMasterSplit= */ false), + createApkDescription( + apkTextureTargeting(ASTC, ImmutableSet.of(ETC2)), + ZipPath.create("asset1-tcf_astc.apk"), + /* isMasterSplit= */ false)); + AssetSliceSet onDemandAssetModule = + createAssetSliceSet( + /* moduleName= */ "asset2", + DeliveryType.ON_DEMAND, + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("asset2-master.apk"))); + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant(lVariant) + .addAssetSliceSet(assetModule) + .addAssetSliceSet(onDemandAssetModule) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + GetSizeCommand.builder() + .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) + .setApksArchivePath(apksArchiveFile) + .setDimensions( + ImmutableSet.of(Dimension.ABI, Dimension.TEXTURE_COMPRESSION_FORMAT, Dimension.SDK)) + .setModules(ImmutableSet.of("asset2")) + .build() + .getSizeTotal(new PrintStream(outputStream)); + + // Selects all install-time modules (base and asset1) and the ones explicitly selected (asset2). + assertThat(new String(outputStream.toByteArray(), UTF_8).split(CRLF)) + .asList() + .containsExactly( + "SDK,ABI,TEXTURE_COMPRESSION_FORMAT,MIN,MAX", + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86_64", "astc", 5 * compressedApkSize, 5 * compressedApkSize), + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86_64", "etc2", 5 * compressedApkSize, 5 * compressedApkSize), + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86", "astc", 5 * compressedApkSize, 5 * compressedApkSize), + String.format( + "%s,%s,%s,%d,%d", + "21-", "x86", "etc2", 5 * compressedApkSize, 5 * compressedApkSize)); + } + @Test public void getSizeTotal_withAssetModulesAndDeviceSpec() throws Exception { Variant lVariant = @@ -1448,6 +1511,74 @@ public void getSizeTotal_withDeviceTier_withDeviceTier() throws Exception { String.format("%s,%s,%d,%d", "25", "1", 2 * compressedApkSize, 2 * compressedApkSize)); } + @Test + public void getSizeTotal_withCountrySet() throws Exception { + Variant lVariant = + createVariant( + lPlusVariantTargeting(), + createSplitApkSet( + /* moduleName= */ "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk")), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", /* alternatives= */ ImmutableList.of("sea"))), + ZipPath.create("base-countries_latam.apk")), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", /* alternatives= */ ImmutableList.of("latam"))), + ZipPath.create("base-countries_sea.apk")))); + AssetSliceSet assetSliceSet = + createAssetSliceSet( + /* moduleName= */ "assetpack1", + DeliveryType.INSTALL_TIME, + createApkDescription( + ApkTargeting.getDefaultInstance(), ZipPath.create("assetpack1-master.apk"), true), + createApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", /* alternatives= */ ImmutableList.of("sea"))), + ZipPath.create("assetpack1-countries_latam.apk"), + false), + createApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", /* alternatives= */ ImmutableList.of("latam"))), + ZipPath.create("assetpack1-countries_sea.apk"), + false)); + + BuildApksResult tableOfContentsProto = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant(lVariant) + .addAssetSliceSet(assetSliceSet) + .build(); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + GetSizeCommand.builder() + .setGetSizeSubCommand(GetSizeSubcommand.TOTAL) + .setApksArchivePath(apksArchiveFile) + .setDimensions(ImmutableSet.of(Dimension.COUNTRY_SET, Dimension.SDK)) + .build() + .getSizeTotal(new PrintStream(outputStream)); + + assertThat(new String(outputStream.toByteArray(), UTF_8).split(CRLF)) + .asList() + .containsExactly( + "SDK,COUNTRY_SET,MIN,MAX", + String.format( + "%s,%s,%d,%d", "21-", "latam", 4 * compressedApkSize, 4 * compressedApkSize), + String.format( + "%s,%s,%d,%d", "21-", "sea", 4 * compressedApkSize, 4 * compressedApkSize)); + } + @Test public void getSizeTotal_defaultDeviceAndSdkApkSet_match() throws Exception { Variant sdkVariant = diff --git a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java index 245c0792..13c85674 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/InstallApksCommandTest.java @@ -34,9 +34,12 @@ import static com.android.tools.build.bundletool.testing.DeviceFactory.sdkVersion; import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_HOME; import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_SERIAL; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDeviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; @@ -262,6 +265,27 @@ public void fromFlagsEquivalentToBuilder_deviceTier() throws Exception { assertThat(fromBuilder).isEqualTo(fromFlags); } + @Test + public void fromFlagsEquivalentToBuilder_countrySet() throws Exception { + InstallApksCommand fromFlags = + InstallApksCommand.fromFlags( + new FlagParser() + .parse("--apks=" + simpleApksPath, "--adb=" + adbPath, "--country-set=latam"), + systemEnvironmentProvider, + fakeServerOneDevice(lDeviceWithLocales("en-US"))); + + InstallApksCommand fromBuilder = + InstallApksCommand.builder() + .setApksArchivePath(simpleApksPath) + .setAdbPath(adbPath) + .setAdbServer(fromFlags.getAdbServer()) + .setDeviceId(DEVICE_ID) + .setCountrySet("latam") + .build(); + + assertThat(fromBuilder).isEqualTo(fromFlags); + } + @Test public void fromFlagsEquivalentToBuilder_timeout() throws Exception { InstallApksCommand fromFlags = @@ -841,9 +865,14 @@ public void installAssetModules(@FromDataPoints("apksInDirectory") boolean apksI .build() .execute(); + // Installs base and all install-time modules. assertThat(getFileNames(installedApks)) .containsExactly( - baseApk.toString(), installTimeMasterApk1.toString(), installTimeEnApk1.toString()); + baseApk.toString(), + installTimeMasterApk1.toString(), + installTimeEnApk1.toString(), + installTimeMasterApk2.toString(), + installTimeEnApk2.toString()); } @Test @@ -1393,6 +1422,189 @@ public void bundleWithDeviceTierTargeting_deviceTierSet_filtersByTier( baseHighApk.toString(), asset1MasterApk.toString(), asset1HighApk.toString()); } + @Test + public void bundleWithCountrySetTargeting_noCountrySetSpecifiedNorDefault_usesFallback() + throws Exception { + ZipPath baseMasterApk = ZipPath.create("base-master.apk"); + ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); + ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); + ZipPath baseLatamApk = ZipPath.create("base-countries_latam.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setPackageName(PKG_NAME) + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), baseMasterApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", + /* alternatives= */ ImmutableList.of("latam"))), + baseSeaApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", + /* alternatives= */ ImmutableList.of("sea"))), + baseLatamApk), + splitApkDescription( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("sea", "latam"))), + baseRestOfWorldApk)))) + .addDefaultTargetingValue( + DefaultTargetingValue.newBuilder().setDimension(Value.COUNTRY_SET)) + .build(); + + Path apksFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + + List installedApks = new ArrayList<>(); + FakeDevice fakeDevice = + FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); + fakeDevice.setInstallApksSideEffect((apks, installOptions) -> installedApks.addAll(apks)); + AdbServer adbServer = + new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); + + InstallApksCommand.builder() + .setApksArchivePath(apksFile) + .setAdbPath(adbPath) + .setAdbServer(adbServer) + .build() + .execute(); + + assertThat(getFileNames(installedApks)) + .containsExactly(baseMasterApk.toString(), baseRestOfWorldApk.toString()); + } + + @Test + public void bundleWithCountrySetTargeting_noCountrySetSpecified_usesDefaults() throws Exception { + ZipPath baseMasterApk = ZipPath.create("base-master.apk"); + ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); + ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); + ZipPath baseLatamApk = ZipPath.create("base-countries_latam.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), baseMasterApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", + /* alternatives= */ ImmutableList.of("latam"))), + baseSeaApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", + /* alternatives= */ ImmutableList.of("sea"))), + baseLatamApk), + splitApkDescription( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("sea", "latam"))), + baseRestOfWorldApk)))) + .addDefaultTargetingValue( + DefaultTargetingValue.newBuilder() + .setDimension(Value.COUNTRY_SET) + .setDefaultValue("latam")) + .build(); + + Path apksFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + List installedApks = new ArrayList<>(); + FakeDevice fakeDevice = + FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); + fakeDevice.setInstallApksSideEffect((apks, installOptions) -> installedApks.addAll(apks)); + AdbServer adbServer = + new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); + + InstallApksCommand.builder() + .setApksArchivePath(apksFile) + .setAdbPath(adbPath) + .setAdbServer(adbServer) + .build() + .execute(); + + assertThat(getFileNames(installedApks)) + .containsExactly(baseMasterApk.toString(), baseLatamApk.toString()); + } + + @Test + public void bundleWithCountrySetTargeting_countrySetSpecified_filterByCountrySet() + throws Exception { + ZipPath baseMasterApk = ZipPath.create("base-master.apk"); + ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); + ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); + ZipPath baseLatamApk = ZipPath.create("base-countries_latam.apk"); + BuildApksResult buildApksResult = + BuildApksResult.newBuilder() + .setBundletool( + Bundletool.newBuilder() + .setVersion(BundleToolVersion.getCurrentVersion().toString())) + .addVariant( + createVariant( + variantSdkTargeting( + sdkVersionFrom(21), ImmutableSet.of(SdkVersion.getDefaultInstance())), + createSplitApkSet( + "base", + createMasterApkDescription( + ApkTargeting.getDefaultInstance(), baseMasterApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "sea", + /* alternatives= */ ImmutableList.of("latam"))), + baseSeaApk), + splitApkDescription( + apkCountrySetTargeting( + countrySetTargeting( + /* value= */ "latam", + /* alternatives= */ ImmutableList.of("sea"))), + baseLatamApk), + splitApkDescription( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("sea", "latam"))), + baseRestOfWorldApk)))) + .addDefaultTargetingValue( + DefaultTargetingValue.newBuilder() + .setDimension(Value.COUNTRY_SET) + .setDefaultValue("latam")) + .build(); + + Path apksFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + List installedApks = new ArrayList<>(); + FakeDevice fakeDevice = + FakeDevice.fromDeviceSpec(DEVICE_ID, DeviceState.ONLINE, lDeviceWithLocales("en-US")); + fakeDevice.setInstallApksSideEffect((apks, installOptions) -> installedApks.addAll(apks)); + AdbServer adbServer = + new FakeAdbServer(/* hasInitialDeviceList= */ true, ImmutableList.of(fakeDevice)); + + InstallApksCommand.builder() + .setApksArchivePath(apksFile) + .setAdbPath(adbPath) + .setAdbServer(adbServer) + .setCountrySet("sea") + .build() + .execute(); + + assertThat(getFileNames(installedApks)) + .containsExactly(baseMasterApk.toString(), baseSeaApk.toString()); + } + @Test public void printHelp_doesNotCrash() { GetDeviceSpecCommand.help(); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommandTest.java index b0131e94..31c1c69d 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/PrintDeviceTargetingConfigCommandTest.java @@ -145,6 +145,18 @@ public void bytesAreRoundedToTwoDecimalPlaces() throws Exception { "selector_with_bytes_not_multiple_of_1024_expected_output.txt"); } + @Test + public void onlyCountrySets_ok() throws Exception { + assertOutputIsExpected("only_country_sets.json", "only_country_sets_expected_output.txt"); + } + + @Test + public void countrySetsWithDeviceTiers_ok() throws Exception { + assertOutputIsExpected( + "country_sets_with_device_tiers.json", + "country_sets_with_device_tiers_expected_output.txt"); + } + @Test public void buildingCommandViaFlags_deviceTargetingConfigPathNotSet_throws() { Throwable e = diff --git a/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java index 2ca6e745..2c64d719 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/ApkMatcherTest.java @@ -1596,6 +1596,29 @@ public void assetModuleMatch() { createMatcher( enDevice, Optional.of(ImmutableSet.of(installTimeModule1, onDemandModule))) .getMatchingApks(buildApksResult)) + .containsExactly( + baseMatchedApk(baseApk), + matchedApk(installTimeMasterApk1, installTimeModule1, INSTALL_TIME), + matchedApk(installTimeEnApk1, installTimeModule1, INSTALL_TIME), + matchedApk(installTimeMasterApk2, installTimeModule2, INSTALL_TIME), + matchedApk(installTimeEnApk2, installTimeModule2, INSTALL_TIME), + matchedApk(onDemandMasterApk, onDemandModule, ON_DEMAND)); + + assertThat( + createMatcher(enDevice, Optional.of(ImmutableSet.of(onDemandModule))) + .getMatchingApks(buildApksResult)) + .containsExactly( + baseMatchedApk(baseApk), + matchedApk(installTimeMasterApk1, installTimeModule1, INSTALL_TIME), + matchedApk(installTimeEnApk1, installTimeModule1, INSTALL_TIME), + matchedApk(installTimeMasterApk2, installTimeModule2, INSTALL_TIME), + matchedApk(installTimeEnApk2, installTimeModule2, INSTALL_TIME), + matchedApk(onDemandMasterApk, onDemandModule, ON_DEMAND)); + + assertThat( + createMatcherExcludingInstallTimeAssetModules( + enDevice, Optional.of(ImmutableSet.of(installTimeModule1, onDemandModule))) + .getMatchingApks(buildApksResult)) .containsExactly( baseMatchedApk(baseApk), matchedApk(installTimeMasterApk1, installTimeModule1, INSTALL_TIME), @@ -1710,18 +1733,40 @@ private static GeneratedApk baseMatchedApk(ZipPath path) { private static ApkMatcher createMatcher(DeviceSpec spec, Optional> modules) { return new ApkMatcher( - spec, modules, /* matchInstant= */ false, /* ensureDensityAndAbiApksMatched= */ false); + spec, + modules, + /* includeInstallTimeAssetModules= */ true, + /* matchInstant= */ false, + /* ensureDensityAndAbiApksMatched= */ false); + } + + private static ApkMatcher createMatcherExcludingInstallTimeAssetModules( + DeviceSpec spec, Optional> modules) { + return new ApkMatcher( + spec, + modules, + /* includeInstallTimeAssetModules= */ false, + /* matchInstant= */ false, + /* ensureDensityAndAbiApksMatched= */ false); } private static ApkMatcher createInstantMatcher( DeviceSpec spec, Optional> modules) { return new ApkMatcher( - spec, modules, /* matchInstant= */ true, /* ensureDensityAndAbiApksMatched= */ false); + spec, + modules, + /* includeInstallTimeAssetModules= */ true, + /* matchInstant= */ true, + /* ensureDensityAndAbiApksMatched= */ false); } private static ApkMatcher createSafeMatcher( DeviceSpec spec, Optional> modules) { return new ApkMatcher( - spec, modules, /* matchInstant= */ false, /* ensureDensityAndAbiApksMatched= */ true); + spec, + modules, + /* includeInstallTimeAssetModules= */ true, + /* matchInstant= */ false, + /* ensureDensityAndAbiApksMatched= */ true); } } diff --git a/src/test/java/com/android/tools/build/bundletool/device/CountrySetApkMatcherTest.java b/src/test/java/com/android/tools/build/bundletool/device/CountrySetApkMatcherTest.java new file mode 100644 index 00000000..26f5c8bb --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/device/CountrySetApkMatcherTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 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.device; + +import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; +import static com.android.tools.build.bundletool.testing.DeviceFactory.countrySet; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Devices.DeviceSpec; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CountrySetApkMatcherTest { + + @Test + public void matchesTargeting_matches() { + CountrySetApkMatcher latamMatcher = new CountrySetApkMatcher(countrySet("latam")); + CountrySetTargeting latamTargetingWithoutAlternatives = countrySetTargeting("latam"); + CountrySetTargeting latamTargetingWithAlternatives = + countrySetTargeting("latam", ImmutableList.of("sea")); + + assertThat(latamMatcher.matchesTargeting(latamTargetingWithoutAlternatives)).isTrue(); + assertThat(latamMatcher.matchesTargeting(latamTargetingWithAlternatives)).isTrue(); + } + + @Test + public void matchesTargeting_doesNotMatch() { + CountrySetApkMatcher latamMatcher = new CountrySetApkMatcher(countrySet("latam")); + CountrySetTargeting seaTargeting = countrySetTargeting("sea", ImmutableList.of("latam")); + + assertThat(latamMatcher.matchesTargeting(seaTargeting)).isFalse(); + } + + @Test + public void matchesTargeting_overlappingValuesAndAlternatives_throws() { + CountrySetApkMatcher matcher = new CountrySetApkMatcher(countrySet("latam")); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + matcher.matchesTargeting( + countrySetTargeting("latam", ImmutableList.of("sea", "latam")))); + assertThat(exception) + .hasMessageThat() + .contains( + "Expected targeting values and alternatives to be mutually exclusive, but both contain:" + + " [latam]"); + } + + @Test + public void getTargetingValue() { + CountrySetApkMatcher latamMatcher = new CountrySetApkMatcher(countrySet("latam")); + CountrySetTargeting latamTargeting = countrySetTargeting("latam", ImmutableList.of("sea")); + ApkTargeting latamApkTargeting = + ApkTargeting.newBuilder().setCountrySetTargeting(latamTargeting).build(); + + assertThat(latamMatcher.getTargetingValue(latamApkTargeting)).isEqualTo(latamTargeting); + } + + @Test + public void isDimensionPresent() { + DeviceSpec deviceSpecWithCountrySet = countrySet("latam"); + DeviceSpec deviceSpecWithoutCountrySet = abis("x86"); + + assertThat(new CountrySetApkMatcher(deviceSpecWithCountrySet).isDeviceDimensionPresent()) + .isTrue(); + assertThat(new CountrySetApkMatcher(deviceSpecWithoutCountrySet).isDeviceDimensionPresent()) + .isFalse(); + } + + @Test + public void checkDeviceCompatibleInternal_isCompatible() { + CountrySetApkMatcher latamMatcher = new CountrySetApkMatcher(countrySet("latam")); + CountrySetTargeting latamTargetingWithoutAlternatives = countrySetTargeting("latam"); + CountrySetTargeting latamTargetingWithAlternatives = + countrySetTargeting("sea", ImmutableList.of("latam", "europe")); + + latamMatcher.checkDeviceCompatibleInternal(latamTargetingWithoutAlternatives); + latamMatcher.checkDeviceCompatibleInternal(latamTargetingWithAlternatives); + } + + @Test + public void checkDeviceCompatibleInternal_isNotCompatible() { + CountrySetApkMatcher latamMatcher = new CountrySetApkMatcher(countrySet("latam")); + CountrySetTargeting targetingWithoutLatamInValuesAndAlternatives = + countrySetTargeting("sea", ImmutableList.of("europe")); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + latamMatcher.checkDeviceCompatibleInternal( + targetingWithoutLatamInValuesAndAlternatives)); + assertThat(exception) + .hasMessageThat() + .contains( + "The specified country set 'latam' does not match any of the available values: sea," + + " europe."); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/device/DeviceSpecUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/device/DeviceSpecUtilsTest.java index b446329e..9b7728f1 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/DeviceSpecUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/DeviceSpecUtilsTest.java @@ -31,6 +31,8 @@ import static com.android.tools.build.bundletool.testing.DeviceFactory.mergeSpecs; import static com.android.tools.build.bundletool.testing.DeviceFactory.sdkVersion; import static com.android.tools.build.bundletool.testing.TargetingUtils.abiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.screenDensityTargeting; @@ -43,6 +45,7 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensityTargeting; import com.android.bundle.Targeting.SdkRuntimeTargeting; @@ -139,12 +142,13 @@ public void deviceSpecFromTargetingBuilder_setSupportedLocales() { } @Test - public void deviceSpecFromTargetingBuilder_setSupportedTextureCompressionFormats_toDefaultInstance() { + public void + deviceSpecFromTargetingBuilder_setSupportedTextureCompressionFormats_toDefaultInstance() { assertThat( - new DeviceSpecFromTargetingBuilder(DeviceSpec.getDefaultInstance()) - .setSupportedTextureCompressionFormats( - TextureCompressionFormatTargeting.getDefaultInstance()) - .build()) + new DeviceSpecFromTargetingBuilder(DeviceSpec.getDefaultInstance()) + .setSupportedTextureCompressionFormats( + TextureCompressionFormatTargeting.getDefaultInstance()) + .build()) .isEqualToDefaultInstance(); } @@ -175,6 +179,38 @@ public void deviceSpecFromTargetingBuilder_setDeviceTier() { assertThat(deviceSpec.getDeviceTier().getValue()).isEqualTo(2); } + @Test + public void deviceSpecFromTargetingBuilder_setCountrySet() { + DeviceSpec deviceSpec = + new DeviceSpecFromTargetingBuilder(DeviceSpec.getDefaultInstance()) + .setCountrySet( + countrySetTargeting( + /* value= */ "latam", /* alternatives= */ ImmutableList.of("sea"))) + .build(); + + assertThat(deviceSpec.getCountrySet().getValue()).isEqualTo("latam"); + } + + @Test + public void deviceSpecFromTargetingBuilder_setCountrySet_onlyAlternatives() { + DeviceSpec deviceSpec = + new DeviceSpecFromTargetingBuilder(DeviceSpec.getDefaultInstance()) + .setCountrySet(alternativeCountrySetTargeting(ImmutableList.of("sea", "latam"))) + .build(); + + assertThat(deviceSpec.getCountrySet().getValue()).isEmpty(); + } + + @Test + public void deviceSpecFromTargetingBuilder_setCountrySet_defaultInstance() { + DeviceSpec deviceSpec = + new DeviceSpecFromTargetingBuilder(DeviceSpec.getDefaultInstance()) + .setCountrySet(CountrySetTargeting.getDefaultInstance()) + .build(); + + assertThat(deviceSpec.hasCountrySet()).isFalse(); + } + @Test public void deviceSpecFromTargetingBuilder_setSdkRuntime() { DeviceSpec deviceSpec = diff --git a/src/test/java/com/android/tools/build/bundletool/mergers/MergingUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/mergers/MergingUtilsTest.java index 472ffc81..47551a65 100644 --- a/src/test/java/com/android/tools/build/bundletool/mergers/MergingUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/mergers/MergingUtilsTest.java @@ -61,8 +61,8 @@ public void mergeShardTargetings_nonAbiNonDensityNonLanguageTargeting_throws() { assertThat(exception) .hasMessageThat() .contains( - "Expecting only ABI, screen density, language and texture compression format" - + " targeting"); + "Expecting only ABI, screen density, language, texture compression format, device tier" + + " and country set targeting"); } @Test @@ -77,8 +77,8 @@ public void mergeShardTargetings_sdkTargetingSecondTargeting_throws() { assertThat(exception) .hasMessageThat() .contains( - "Expecting only ABI, screen density, language and texture compression format" - + " targeting"); + "Expecting only ABI, screen density, language, texture compression format, device tier" + + " and country set targeting"); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java index 1cb5788f..dd97ec57 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ModuleSplitTest.java @@ -34,12 +34,14 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.abiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDeviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkMultiAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkSanitizerTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkTextureTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; @@ -257,6 +259,38 @@ public void moduleDeviceTierSplitSuffixAndName() { assertThat(deviceTieredSplit.getAndroidManifest().getSplitId()).hasValue("config.tier_0"); } + @Test + public void moduleCountrySetSplitSuffixAndName() { + ModuleSplit countrySetSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setVariantTargeting(lPlusVariantTargeting()) + .setApkTargeting(apkCountrySetTargeting(countrySetTargeting("latam"))) + .setMasterSplit(false) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .build(); + countrySetSplit = countrySetSplit.writeSplitIdInManifest(countrySetSplit.getSuffix()); + assertThat(countrySetSplit.getAndroidManifest().getSplitId()) + .hasValue("config.countries_latam"); + } + + @Test + public void moduleCountrySetSplitSuffixAndName_alternatives() { + ModuleSplit countrySetSplit = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setVariantTargeting(lPlusVariantTargeting()) + .setApkTargeting( + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of(), ImmutableList.of("latam", "sea")))) + .setMasterSplit(false) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .build(); + countrySetSplit = countrySetSplit.writeSplitIdInManifest(countrySetSplit.getSuffix()); + assertThat(countrySetSplit.getAndroidManifest().getSplitId()) + .hasValue("config.other_countries"); + } + @Test public void apexModuleMultiAbiSplitSuffixAndName() { ModuleSplit resSplit = diff --git a/src/test/java/com/android/tools/build/bundletool/model/SizeConfigurationTest.java b/src/test/java/com/android/tools/build/bundletool/model/SizeConfigurationTest.java index bc7fd7ba..42b8c72c 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/SizeConfigurationTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/SizeConfigurationTest.java @@ -25,6 +25,7 @@ import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ETC2; import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.PVRTC; import static com.android.tools.build.bundletool.model.SizeConfiguration.getAbiName; +import static com.android.tools.build.bundletool.model.SizeConfiguration.getCountrySetName; import static com.android.tools.build.bundletool.model.SizeConfiguration.getDeviceTierLevel; import static com.android.tools.build.bundletool.model.SizeConfiguration.getLocaleName; import static com.android.tools.build.bundletool.model.SizeConfiguration.getScreenDensityName; @@ -32,6 +33,8 @@ import static com.android.tools.build.bundletool.model.SizeConfiguration.getSdkRuntimeRequired; import static com.android.tools.build.bundletool.model.SizeConfiguration.getTextureCompressionFormatName; import static com.android.tools.build.bundletool.testing.TargetingUtils.abiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.screenDensityTargeting; @@ -43,6 +46,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Targeting.AbiTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.ScreenDensityTargeting; import com.android.bundle.Targeting.SdkRuntimeTargeting; @@ -161,6 +165,23 @@ public void getDeviceTier_singleDevicetierTargeting() { .hasValue(1); } + @Test + public void getCountrySetName_onlyAlternativesIn_countrySetTargeting() { + assertThat(getCountrySetName(alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))) + .hasValue(""); + } + + @Test + public void getCountrySet_emptyCountrySetTargeting() { + assertThat(getCountrySetName(CountrySetTargeting.getDefaultInstance())).isEmpty(); + } + + @Test + public void getCountrySet_withValuesAndAlternativesIn_countrySetTargeting() { + assertThat(getCountrySetName(countrySetTargeting("sea", ImmutableList.of("latam", "europe")))) + .hasValue("sea"); + } + @Test public void getSdkRuntime() { assertThat( diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java index 2e7e8d20..5e326dd6 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.model.targeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; @@ -201,6 +202,41 @@ public void testTargeting_deviceTier_invalidTierName_notAnInt() { InvalidBundleException.class, () -> TargetedDirectorySegment.parse("test#tier_1.5")); } + @Test + public void testTargeting_countrySet() { + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#countries_latam"); + assertThat(segment.getName()).isEqualTo("test"); + assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.COUNTRY_SET); + assertThat(segment.getTargeting()) + .isEqualTo(assetsDirectoryTargeting(countrySetTargeting("latam"))); + } + + @Test + public void testTargeting_countrySet_invalidCountrySetName_withSpecialChars() { + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> TargetedDirectorySegment.parse("assets/test#countries_latam$%@#")); + assertThat(exception) + .hasMessageThat() + .contains( + "Country set name should match the regex '^[a-zA-Z][a-zA-Z0-9_]*$' but got" + + " 'latam$%@#' for directory 'assets/test'."); + } + + @Test + public void testTargeting_countrySet_invalidCountrySetName_onlyNumerics() { + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> TargetedDirectorySegment.parse("assets/test#countries_1234")); + assertThat(exception) + .hasMessageThat() + .contains( + "Country set name should match the regex '^[a-zA-Z][a-zA-Z0-9_]*$' but got" + + " '1234' for directory 'assets/test'."); + } + @Test public void testFailsParsing_missingKey() { assertThrows(InvalidBundleException.class, () -> TargetedDirectorySegment.parse("bad#")); @@ -293,4 +329,13 @@ public void testTargeting_deviceTier_toPathIdempotent() { segment = TargetedDirectorySegment.parse("test#tier_0"); assertThat(segment.toPathSegment()).isEqualTo("test#tier_0"); } + + @Test + public void testTargeting_countrySet_toPathIdempotent() { + TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#countries_latam"); + assertThat(segment.toPathSegment()).isEqualTo("test#countries_latam"); + + segment = TargetedDirectorySegment.parse("test#countries_sea"); + assertThat(segment.toPathSegment()).isEqualTo("test#countries_sea"); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingUtilsTest.java index b8d9aae4..9f5ba360 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingUtilsTest.java @@ -20,6 +20,7 @@ import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getMinSdk; import static com.android.tools.build.bundletool.testing.TargetingUtils.abiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeAssetsTargeting; @@ -82,6 +83,14 @@ public void getDimensions_deviceTier() { .containsExactly(TargetingDimension.DEVICE_TIER); } + @Test + public void getDimensions_countrySet() { + assertThat( + TargetingUtils.getTargetingDimensions( + assetsDirectoryTargeting(countrySetTargeting("latam")))) + .containsExactly(TargetingDimension.COUNTRY_SET); + } + @Test public void getDimensions_multiple() { assertThat( diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java index 5d02cc9f..182e870b 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ConfigurationSizesMergerTest.java @@ -188,6 +188,65 @@ public void merge_disjointDimensions() throws Exception { .isEqualTo(expectedMergedConfigurationSizes.getMaxSizeConfigurationMap()); } + @Test + public void merge_configurationSizes_countrySet() throws Exception { + // Arrange + final long config1DefaultMin = 1 << 0; + final long config1SeaMin = 1 << 1; + final long config1DefaultMax = 1 << 2; + final long config1SeaMax = 1 << 3; + final long config2DefaultMin = 1 << 4; + final long config2SeaMin = 1 << 5; + final long config2DefaultMax = 1 << 6; + final long config2SeaMax = 1 << 7; + ConfigurationSizes configurationSizes1 = + ConfigurationSizes.create( + ImmutableMap.of( + SizeConfiguration.builder().setCountrySet("").build(), + config1DefaultMin, + SizeConfiguration.builder().setCountrySet("sea").build(), + config1SeaMin), + ImmutableMap.of( + SizeConfiguration.builder().setCountrySet("").build(), + config1DefaultMax, + SizeConfiguration.builder().setCountrySet("sea").build(), + config1SeaMax)); + ConfigurationSizes configurationSizes2 = + ConfigurationSizes.create( + ImmutableMap.of( + SizeConfiguration.builder().setCountrySet("").build(), + config2DefaultMin, + SizeConfiguration.builder().setCountrySet("sea").build(), + config2SeaMin), + ImmutableMap.of( + SizeConfiguration.builder().setCountrySet("").build(), + config2DefaultMax, + SizeConfiguration.builder().setCountrySet("sea").build(), + config2SeaMax)); + ConfigurationSizes expectedMergedConfigurationSizes = + ConfigurationSizes.create( + ImmutableMap.of( + SizeConfiguration.builder().setCountrySet("").build(), + config1DefaultMin + config2DefaultMin, + SizeConfiguration.builder().setCountrySet("sea").build(), + config1SeaMin + config2SeaMin), + ImmutableMap.of( + SizeConfiguration.builder().setCountrySet("").build(), + config1DefaultMax + config2DefaultMax, + SizeConfiguration.builder().setCountrySet("sea").build(), + config1SeaMax + config2SeaMax)); + + // Act + ConfigurationSizes actualMergedConfigurationSizes = + ConfigurationSizesMerger.merge(configurationSizes1, configurationSizes2); + + // Assert + assertThat(actualMergedConfigurationSizes.getMinSizeConfigurationMap()) + .isEqualTo(expectedMergedConfigurationSizes.getMinSizeConfigurationMap()); + assertThat(actualMergedConfigurationSizes.getMaxSizeConfigurationMap()) + .isEqualTo(expectedMergedConfigurationSizes.getMaxSizeConfigurationMap()); + } + @Test public void merge_sdkRuntimeWithDensity_allCombinations() { final long runtimeEnabledMin = 1; diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtilsTest.java index 19302feb..3989a9f5 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/GetSizeCsvUtilsTest.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.model.utils; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.ABI; +import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.COUNTRY_SET; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.DEVICE_TIER; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.LANGUAGE; import static com.android.tools.build.bundletool.model.GetSizeRequest.Dimension.SCREEN_DENSITY; @@ -104,6 +105,7 @@ public void getSizeTotalOutputInCsv_withDimensionsAndCommasInConfiguration() { .setLocale("en,fr") .setTextureCompressionFormat("ASTC,ETC2") .setDeviceTier(1) + .setCountrySet("latam") .setSdkRuntime("Not Required") .build(), 1L), @@ -115,6 +117,7 @@ public void getSizeTotalOutputInCsv_withDimensionsAndCommasInConfiguration() { .setLocale("en,fr") .setTextureCompressionFormat("ASTC,ETC2") .setDeviceTier(1) + .setCountrySet("latam") .setSdkRuntime("Not Required") .build(), 6L)), @@ -125,12 +128,13 @@ public void getSizeTotalOutputInCsv_withDimensionsAndCommasInConfiguration() { SDK, TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER, + COUNTRY_SET, SDK_RUNTIME), SizeFormatter.rawFormatter())) .isEqualTo( - "SDK,ABI,SCREEN_DENSITY,LANGUAGE,TEXTURE_COMPRESSION_FORMAT,DEVICE_TIER,SDK_RUNTIME,MIN,MAX" + "SDK,ABI,SCREEN_DENSITY,LANGUAGE,TEXTURE_COMPRESSION_FORMAT,DEVICE_TIER,COUNTRY_SET,SDK_RUNTIME,MIN,MAX" + CRLF - + "22,\"x86,armeabi-v7a\",480,\"en,fr\",\"ASTC,ETC2\",1,Not Required,1,6" + + "22,\"x86,armeabi-v7a\",480,\"en,fr\",\"ASTC,ETC2\",1,latam,Not Required,1,6" + CRLF); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtilsTest.java index 5c374d98..10c6437e 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingProtoUtilsTest.java @@ -17,9 +17,11 @@ package com.android.tools.build.bundletool.model.utils; import static com.android.tools.build.bundletool.testing.TargetingUtils.abiTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeDeviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeAssetsTargeting; @@ -75,6 +77,15 @@ public void toAlternativeTargeting_deviceTier() { .isEqualTo(assetsDirectoryTargeting(alternativeDeviceTierTargeting(ImmutableList.of(1)))); } + @Test + public void toAlternativeTargeting_countrySet() { + assertThat( + TargetingProtoUtils.toAlternativeTargeting( + assetsDirectoryTargeting(countrySetTargeting("latam")))) + .isEqualTo( + assetsDirectoryTargeting(alternativeCountrySetTargeting(ImmutableList.of("latam")))); + } + @Test public void toAlternativeTargeting_multipleDimensionsAndValues() { assertThat( diff --git a/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java b/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java index ca59aba8..e066c8e1 100644 --- a/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/optimizations/ApkOptimizationsTest.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.optimizations; import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; +import static com.android.tools.build.bundletool.model.OptimizationDimension.COUNTRY_SET; import static com.android.tools.build.bundletool.model.OptimizationDimension.DEVICE_TIER; import static com.android.tools.build.bundletool.model.OptimizationDimension.LANGUAGE; import static com.android.tools.build.bundletool.model.OptimizationDimension.SCREEN_DENSITY; @@ -95,11 +96,12 @@ public void getSplitDimensionsForAssetModules_returnsDimensionsSupportedByAssetM ApkOptimizations optimizations = ApkOptimizations.builder() .setSplitDimensions( - ImmutableSet.of(ABI, SCREEN_DENSITY, TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER)) + ImmutableSet.of( + ABI, SCREEN_DENSITY, TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER, COUNTRY_SET)) .setStandaloneDimensions(ImmutableSet.of(ABI)) .build(); assertThat(optimizations.getSplitDimensionsForAssetModules()) - .containsExactly(TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER); + .containsExactly(TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER, COUNTRY_SET); } } diff --git a/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java b/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java index f5069c0e..e55f6120 100644 --- a/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java +++ b/src/test/java/com/android/tools/build/bundletool/shards/SuffixStripperTest.java @@ -19,11 +19,14 @@ import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ATC; import static com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias.ETC1_RGB8; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeTextureCompressionTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDeviceTierTargeting; 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; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedAssetsDirectory; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; @@ -297,6 +300,149 @@ public void applySuffixStripping_deviceTier_suffixStrippingDisabled_nonDefaultVa assertThat(strippedSplit.getVariantTargeting()).isEqualToDefaultInstance(); } + @Test + public void applySuffixStripping_countrySet_suffixStrippingEnabled() { + ModuleSplit split = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .setMasterSplit(true) + .setEntries( + ImmutableList.of( + createModuleEntryForFile( + "assets/img#countries_latam/img_latam.dat", TEST_CONTENT), + createModuleEntryForFile("assets/img#countries_sea/img_sea.dat", TEST_CONTENT))) + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/img#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/img#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))))) + .build(); + + ModuleSplit strippedSplit = + SuffixStripper.createForDimension(TargetingDimension.COUNTRY_SET) + .applySuffixStripping( + split, + SuffixStripping.newBuilder().setDefaultSuffix("latam").setEnabled(true).build()); + + assertThat(strippedSplit.getEntries()).hasSize(1); + assertThat(strippedSplit.getEntries().get(0).getPath()) + .isEqualTo(ZipPath.create("assets/img/img_latam.dat")); + + assertThat(strippedSplit.getAssetsConfig().get().getDirectoryCount()).isEqualTo(1); + assertThat(strippedSplit.getAssetsConfig().get().getDirectory(0).getPath()) + .isEqualTo("assets/img"); + + assertThat(strippedSplit.getApkTargeting()) + .isEqualTo(apkCountrySetTargeting(countrySetTargeting("latam"))); + assertThat(strippedSplit.getVariantTargeting()).isEqualToDefaultInstance(); + } + + @Test + public void applySuffixStripping_countrySet_suffixStrippingDisabled() { + ModuleSplit split = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .setMasterSplit(true) + .setEntries( + ImmutableList.of( + createModuleEntryForFile( + "assets/img#countries_latam/img_latam.dat", TEST_CONTENT), + createModuleEntryForFile("assets/img#countries_sea/img_sea.dat", TEST_CONTENT))) + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/img#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/img#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))))) + .build(); + + ModuleSplit strippedSplit = + SuffixStripper.createForDimension(TargetingDimension.COUNTRY_SET) + .applySuffixStripping( + split, + SuffixStripping.newBuilder().setDefaultSuffix("latam").setEnabled(false).build()); + + assertThat(strippedSplit.getEntries()).hasSize(1); + assertThat(strippedSplit.getEntries().get(0).getPath()) + .isEqualTo(ZipPath.create("assets/img#countries_latam/img_latam.dat")); + + assertThat(strippedSplit.getAssetsConfig().get().getDirectoryCount()).isEqualTo(1); + assertThat(strippedSplit.getAssetsConfig().get().getDirectory(0).getPath()) + .isEqualTo("assets/img#countries_latam"); + + assertThat(strippedSplit.getApkTargeting()) + .isEqualTo(apkCountrySetTargeting(countrySetTargeting("latam"))); + assertThat(strippedSplit.getVariantTargeting()).isEqualToDefaultInstance(); + } + + @Test + public void applySuffixStripping_countrySet_noDefaultSuffixSpecified_retainsFallbackAssets() { + ModuleSplit split = + ModuleSplit.builder() + .setModuleName(BundleModuleName.create("base")) + .setApkTargeting(ApkTargeting.getDefaultInstance()) + .setVariantTargeting(VariantTargeting.getDefaultInstance()) + .setAndroidManifest(AndroidManifest.create(androidManifest("com.test.app"))) + .setMasterSplit(true) + .setEntries( + ImmutableList.of( + createModuleEntryForFile( + "assets/img#countries_latam/img_latam.dat", TEST_CONTENT), + createModuleEntryForFile("assets/img#countries_sea/img_sea.dat", TEST_CONTENT), + createModuleEntryForFile("assets/img/img_restOfWorld.dat", TEST_CONTENT))) + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/img#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/img#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))), + targetedAssetsDirectory( + "assets/img", + assetsDirectoryTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))))) + .build(); + + ModuleSplit strippedSplit = + SuffixStripper.createForDimension(TargetingDimension.COUNTRY_SET) + .applySuffixStripping(split, SuffixStripping.newBuilder().setEnabled(true).build()); + + assertThat(strippedSplit.getEntries()).hasSize(1); + assertThat(strippedSplit.getEntries().get(0).getPath()) + .isEqualTo(ZipPath.create("assets/img/img_restOfWorld.dat")); + + assertThat(strippedSplit.getAssetsConfig().get().getDirectoryCount()).isEqualTo(1); + assertThat(strippedSplit.getAssetsConfig().get().getDirectory(0).getPath()) + .isEqualTo("assets/img"); + + assertThat(strippedSplit.getApkTargeting()).isEqualToDefaultInstance(); + assertThat(strippedSplit.getVariantTargeting()).isEqualToDefaultInstance(); + } + @Test public void removeAssetsTargeting_tcf() { ModuleSplit split = 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 aa3d44c6..d05284e9 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 @@ -16,13 +16,17 @@ package com.android.tools.build.bundletool.splitters; +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.androidManifestForAssetModule; +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.apkTextureTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.assets; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeApkTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.mergeAssetsTargeting; @@ -81,4 +85,98 @@ public void singleSlice() throws Exception { .containsExactly("assets/image.jpg", "assets/image2.jpg"); } + + @Test + public void slicesByCountrySet() throws Exception { + BundleModule testModule = + new BundleModuleBuilder(MODULE_NAME) + .addFile("assets/images/image.jpg") + .addFile("assets/images#countries_latam/image.jpg") + .addFile("assets/images#countries_sea/image.jpg") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/images#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/images#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))), + targetedAssetsDirectory( + "assets/images", + assetsDirectoryTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))))) + .setManifest(androidManifestForAssetModule("com.test.app")) + .build(); + + assertThat(testModule.getModuleType()).isEqualTo(ModuleType.ASSET_MODULE); + ImmutableList slices = + new AssetModuleSplitter( + testModule, + ApkGenerationConfiguration.builder() + .setOptimizationDimensions(ImmutableSet.of(COUNTRY_SET)) + .build()) + .splitModule(); + + assertThat(slices).hasSize(4); + + ImmutableMap slicesByTargeting = + Maps.uniqueIndex(slices, ModuleSplit::getApkTargeting); + + assertThat(slicesByTargeting.keySet()) + .containsExactly( + ApkTargeting.getDefaultInstance(), + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("latam"), ImmutableList.of("sea"))), + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("sea"), ImmutableList.of("latam"))), + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))); + + ModuleSplit masterSplit = slicesByTargeting.get(ApkTargeting.getDefaultInstance()); + assertThat(masterSplit.getSplitType()).isEqualTo(SplitType.ASSET_SLICE); + assertThat(masterSplit.isMasterSplit()).isTrue(); + assertThat(masterSplit.getAndroidManifest().getSplitId()).hasValue(MODULE_NAME); + assertThat(masterSplit.getAndroidManifest().getHasCode()).hasValue(false); + assertThat(masterSplit.getEntries()).isEmpty(); + + ModuleSplit latamSplit = + slicesByTargeting.get( + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("latam"), ImmutableList.of("sea")))); + assertThat(latamSplit.getSplitType()).isEqualTo(SplitType.ASSET_SLICE); + assertThat(latamSplit.isMasterSplit()).isFalse(); + assertThat(latamSplit.getAndroidManifest().getSplitId()) + .hasValue(MODULE_NAME + ".config.countries_latam"); + assertThat(latamSplit.getAndroidManifest().getHasCode()).hasValue(false); + assertThat(extractPaths(latamSplit.getEntries())) + .containsExactly("assets/images#countries_latam/image.jpg"); + + ModuleSplit seaSplit = + slicesByTargeting.get( + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("sea"), ImmutableList.of("latam")))); + assertThat(seaSplit.getSplitType()).isEqualTo(SplitType.ASSET_SLICE); + assertThat(seaSplit.isMasterSplit()).isFalse(); + assertThat(seaSplit.getAndroidManifest().getSplitId()) + .hasValue(MODULE_NAME + ".config.countries_sea"); + assertThat(seaSplit.getAndroidManifest().getHasCode()).hasValue(false); + assertThat(extractPaths(seaSplit.getEntries())) + .containsExactly("assets/images#countries_sea/image.jpg"); + + ModuleSplit restOfWorldSplit = + slicesByTargeting.get( + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))); + assertThat(restOfWorldSplit.getSplitType()).isEqualTo(SplitType.ASSET_SLICE); + assertThat(restOfWorldSplit.isMasterSplit()).isFalse(); + assertThat(restOfWorldSplit.getAndroidManifest().getSplitId()) + .hasValue(MODULE_NAME + ".config.other_countries"); + assertThat(restOfWorldSplit.getAndroidManifest().getHasCode()).hasValue(false); + assertThat(extractPaths(restOfWorldSplit.getEntries())) + .containsExactly("assets/images/image.jpg"); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/CountrySetAssetsSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/CountrySetAssetsSplitterTest.java new file mode 100644 index 00000000..05116480 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/splitters/CountrySetAssetsSplitterTest.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2022 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.splitters; + +import static com.android.tools.build.bundletool.model.BundleModule.ASSETS_DIRECTORY; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +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.assets; +import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.getSplitsWithDefaultTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.getSplitsWithTargetingEqualTo; +import static com.android.tools.build.bundletool.testing.TargetingUtils.targetedAssetsDirectory; +import static com.android.tools.build.bundletool.testing.TestUtils.extractPaths; +import static com.google.common.truth.Truth.assertThat; + +import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CountrySetAssetsSplitter}. */ +@RunWith(JUnit4.class) +public class CountrySetAssetsSplitterTest { + + @Test + public void multipleCountrySetsAndUntargetedFile() { + BundleModule testModule = + new BundleModuleBuilder("testModule") + .addFile("assets/images#countries_latam/image.jpg") + .addFile("assets/images#countries_sea/image.jpg") + .addFile("assets/images/image.jpg") + .addFile("assets/file.txt") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/images#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/images#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))), + targetedAssetsDirectory( + "assets/images", + assetsDirectoryTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))), + targetedAssetsDirectory( + "assets", AssetsDirectoryTargeting.getDefaultInstance()))) + .setManifest(androidManifest("com.test.app")) + .build(); + + ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); + ImmutableCollection assetsSplits = + CountrySetAssetsSplitter.create(/* stripTargetingSuffix= */ false).split(baseSplit); + + assertThat(assetsSplits).hasSize(4); + ImmutableList defaultSplits = getSplitsWithDefaultTargeting(assetsSplits); + assertThat(defaultSplits).hasSize(1); + assertThat(extractPaths(defaultSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/file.txt"); + + ImmutableList latamSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("latam"), ImmutableList.of("sea")))); + assertThat(latamSplits).hasSize(1); + assertThat(extractPaths(latamSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images#countries_latam/image.jpg"); + + ImmutableList seaSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("sea"), ImmutableList.of("latam")))); + assertThat(seaSplits).hasSize(1); + assertThat(extractPaths(seaSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images#countries_sea/image.jpg"); + + ImmutableList restOfWorldSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))); + assertThat(restOfWorldSplits).hasSize(1); + assertThat(extractPaths(restOfWorldSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images/image.jpg"); + } + + @Test + public void countrySet_restOfWorldFolderMissing_ok() { + BundleModule testModule = + new BundleModuleBuilder("testModule") + .addFile("assets/images#countries_latam/image.jpg") + .addFile("assets/images#countries_sea/image.jpg") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/images#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/images#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))))) + .setManifest(androidManifest("com.test.app")) + .build(); + + ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); + ImmutableCollection assetsSplits = + CountrySetAssetsSplitter.create(/* stripTargetingSuffix= */ false).split(baseSplit); + + assertThat(assetsSplits).hasSize(3); + ImmutableList defaultSplits = getSplitsWithDefaultTargeting(assetsSplits); + assertThat(defaultSplits).hasSize(1); + assertThat(extractPaths(defaultSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))).isEmpty(); + + ImmutableList latamSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("latam"), ImmutableList.of("sea")))); + assertThat(latamSplits).hasSize(1); + assertThat(extractPaths(latamSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images#countries_latam/image.jpg"); + + ImmutableList seaSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("sea"), ImmutableList.of("latam")))); + assertThat(seaSplits).hasSize(1); + assertThat(extractPaths(seaSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images#countries_sea/image.jpg"); + } + + @Test + public void multipleCountrySets_withSuffixStripping() { + BundleModule testModule = + new BundleModuleBuilder("testModule") + .addFile("assets/images#countries_latam/image.jpg") + .addFile("assets/images#countries_sea/image.jpg") + .addFile("assets/images/image.jpg") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/images#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/images#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))), + targetedAssetsDirectory( + "assets/images", + assetsDirectoryTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))))) + .setManifest(androidManifest("com.test.app")) + .build(); + + ModuleSplit baseSplit = ModuleSplit.forAssets(testModule); + ImmutableCollection assetsSplits = + CountrySetAssetsSplitter.create(/* stripTargetingSuffix= */ true).split(baseSplit); + + assertThat(assetsSplits).hasSize(4); + ImmutableList defaultSplits = getSplitsWithDefaultTargeting(assetsSplits); + assertThat(defaultSplits).hasSize(1); + assertThat(extractPaths(defaultSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))).isEmpty(); + + ImmutableList latamSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("latam"), ImmutableList.of("sea")))); + assertThat(latamSplits).hasSize(1); + assertThat(extractPaths(latamSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images/image.jpg"); + + ImmutableList seaSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("sea"), ImmutableList.of("latam")))); + assertThat(seaSplits).hasSize(1); + assertThat(extractPaths(seaSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images/image.jpg"); + + ImmutableList restOfWorldSplits = + getSplitsWithTargetingEqualTo( + assetsSplits, + apkCountrySetTargeting( + alternativeCountrySetTargeting(ImmutableList.of("latam", "sea")))); + assertThat(restOfWorldSplits).hasSize(1); + assertThat(extractPaths(restOfWorldSplits.get(0).findEntriesUnderPath(ASSETS_DIRECTORY))) + .containsExactly("assets/images/image.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 ce374736..fed3c1ec 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 @@ -29,6 +29,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.ManifestMutator.withExtractNativeLibs; import static com.android.tools.build.bundletool.model.OptimizationDimension.ABI; +import static com.android.tools.build.bundletool.model.OptimizationDimension.COUNTRY_SET; import static com.android.tools.build.bundletool.model.OptimizationDimension.DEVICE_TIER; import static com.android.tools.build.bundletool.model.OptimizationDimension.LANGUAGE; import static com.android.tools.build.bundletool.model.OptimizationDimension.SCREEN_DENSITY; @@ -68,6 +69,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeLanguageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAlternativeLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDensityTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkDeviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkLanguageTargeting; @@ -75,6 +77,7 @@ 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; +import static com.android.tools.build.bundletool.testing.TargetingUtils.countrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.deviceTierTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.getSplitsWithTargetingEqualTo; import static com.android.tools.build.bundletool.testing.TargetingUtils.lPlusVariantTargeting; @@ -1366,6 +1369,69 @@ public void deviceTierAsset_splitting_and_merging() { .containsExactly("assets/main#tier_1/image.jpg"); } + @Test + public void countrySetAsset_splitting_and_merging() { + BundleModule testModule = + new BundleModuleBuilder("testModule") + .addFile("assets/main#countries_latam/image.jpg") + .addFile("assets/main#countries_sea/image.jpg") + .addFile("dex/classes.dex") + .setAssetsConfig( + assets( + targetedAssetsDirectory( + "assets/main#countries_latam", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("latam"), ImmutableList.of("sea")))), + targetedAssetsDirectory( + "assets/main#countries_sea", + assetsDirectoryTargeting( + countrySetTargeting( + ImmutableList.of("sea"), ImmutableList.of("latam")))))) + .setManifest(androidManifest("com.test.app")) + .build(); + + ImmutableList splits = createCountrySetSplitter(testModule).splitModule(); + + // expected 3 splits: latam, sea and the master split. + assertThat(splits).hasSize(3); + assertThat(splits.stream().map(ModuleSplit::getSplitType).distinct().collect(toImmutableSet())) + .containsExactly(SplitType.SPLIT); + assertThat( + splits.stream() + .map(ModuleSplit::getVariantTargeting) + .distinct() + .collect(toImmutableSet())) + .containsExactly(lPlusVariantTargeting()); + + ImmutableList defaultSplits = + getSplitsWithTargetingEqualTo(splits, DEFAULT_MASTER_SPLIT_SDK_TARGETING); + assertThat(defaultSplits).hasSize(1); + assertThat(extractPaths(defaultSplits.get(0).getEntries())).containsExactly("dex/classes.dex"); + + ImmutableList latamSplits = + getSplitsWithTargetingEqualTo( + splits, + mergeApkTargeting( + DEFAULT_MASTER_SPLIT_SDK_TARGETING, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("latam"), ImmutableList.of("sea"))))); + assertThat(latamSplits).hasSize(1); + assertThat(extractPaths(latamSplits.get(0).getEntries())) + .containsExactly("assets/main#countries_latam/image.jpg"); + + ImmutableList seaSplits = + getSplitsWithTargetingEqualTo( + splits, + mergeApkTargeting( + DEFAULT_MASTER_SPLIT_SDK_TARGETING, + apkCountrySetTargeting( + countrySetTargeting(ImmutableList.of("sea"), ImmutableList.of("latam"))))); + assertThat(seaSplits).hasSize(1); + assertThat(extractPaths(seaSplits.get(0).getEntries())) + .containsExactly("assets/main#countries_sea/image.jpg"); + } + @Test public void targetsPreLOnlyInManifest_throws() throws Exception { int preL = 20; @@ -2415,6 +2481,16 @@ private static ModuleSplitter createDeviceTierSplitter(BundleModule module) { ImmutableSet.of(module.getName().getName())); } + private static ModuleSplitter createCountrySetSplitter(BundleModule module) { + return ModuleSplitter.createNoStamp( + module, + BUNDLETOOL_VERSION, + APP_BUNDLE, + withOptimizationDimensions(ImmutableSet.of(COUNTRY_SET)), + lPlusVariantTargeting(), + ImmutableSet.of(module.getName().getName())); + } + private static ModuleSplitter createAbiAndDensitySplitter( BundleModule module, AppBundle appBundle) { return ModuleSplitter.createNoStamp( diff --git a/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java b/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java index f7fe66a3..3f9f7325 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/DeviceFactory.java @@ -25,6 +25,7 @@ import com.android.bundle.Targeting.ScreenDensity.DensityAlias; import com.android.tools.build.bundletool.model.utils.Versions; import com.google.protobuf.Int32Value; +import com.google.protobuf.StringValue; import com.google.protobuf.util.JsonFormat; import java.nio.file.Files; import java.nio.file.Path; @@ -143,6 +144,10 @@ public static DeviceSpec deviceTier(int deviceTier) { return DeviceSpec.newBuilder().setDeviceTier(Int32Value.of(deviceTier)).build(); } + public static DeviceSpec countrySet(String countrySet) { + return DeviceSpec.newBuilder().setCountrySet(StringValue.of(countrySet)).build(); + } + public static DeviceSpec deviceGroups(String... deviceGroups) { return DeviceSpec.newBuilder().addAllDeviceGroups(Arrays.asList(deviceGroups)).build(); } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java index 646ec9d7..708edebf 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TargetingUtils.java @@ -34,6 +34,7 @@ import com.android.bundle.Targeting.ApexImageTargeting; import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceFeature; import com.android.bundle.Targeting.DeviceFeatureTargeting; import com.android.bundle.Targeting.DeviceGroupModuleTargeting; @@ -121,6 +122,11 @@ public static AssetsDirectoryTargeting assetsDirectoryTargeting( return AssetsDirectoryTargeting.newBuilder().setDeviceTier(deviceTierTargeting).build(); } + public static AssetsDirectoryTargeting assetsDirectoryTargeting( + CountrySetTargeting countrySetTargeting) { + return AssetsDirectoryTargeting.newBuilder().setCountrySet(countrySetTargeting).build(); + } + // Native.pb helper methods. public static NativeLibraries nativeLibraries(TargetedNativeDirectory... nativeDirectories) { @@ -375,6 +381,10 @@ public static ApkTargeting apkDeviceTierTargeting(DeviceTierTargeting deviceTier return ApkTargeting.newBuilder().setDeviceTierTargeting(deviceTierTargeting).build(); } + public static ApkTargeting apkCountrySetTargeting(CountrySetTargeting countrySetTargeting) { + return ApkTargeting.newBuilder().setCountrySetTargeting(countrySetTargeting).build(); + } + // Variant Targeting helpers. Should be written in terms of existing targeting dimension protos or // helpers. See below, for the targeting dimension helper methods. @@ -863,6 +873,33 @@ public static DeviceTierTargeting alternativeDeviceTierTargeting( .build(); } + // Country Set targeting. + + public static CountrySetTargeting countrySetTargeting(String value) { + return CountrySetTargeting.newBuilder().addValue(value).build(); + } + + public static CountrySetTargeting countrySetTargeting( + String value, ImmutableList alternatives) { + return CountrySetTargeting.newBuilder() + .addValue(value) + .addAllAlternatives(alternatives) + .build(); + } + + public static CountrySetTargeting countrySetTargeting( + ImmutableList value, ImmutableList alternatives) { + return CountrySetTargeting.newBuilder() + .addAllValue(value) + .addAllAlternatives(alternatives) + .build(); + } + + public static CountrySetTargeting alternativeCountrySetTargeting( + ImmutableList alternatives) { + return CountrySetTargeting.newBuilder().addAllAlternatives(alternatives).build(); + } + // Device Feature targeting. public static DeviceFeatureTargeting deviceFeatureTargeting(String featureName) { diff --git a/src/test/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidatorTest.java index 7a1a2c63..ed4bcffb 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/AssetsTargetingValidatorTest.java @@ -33,6 +33,7 @@ import com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.TextureCompressionFormat; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; @@ -235,6 +236,30 @@ public void validateModule_defaultInstanceOfTcfTargeting_throws() throws Excepti assertThat(e).hasMessageThat().contains("set but empty Texture Compression Format targeting"); } + @Test + public void validateModule_defaultInstanceOfCountrySetTargeting_throws() throws Exception { + Assets config = + assets( + targetedAssetsDirectory( + "assets/dir#countries_latam", + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(CountrySetTargeting.getDefaultInstance()) + .build())); + BundleModule module = + new BundleModuleBuilder("testModule") + .addFile("assets/dir#countries_latam/raw.dat") + .setAssetsConfig(config) + .setManifest(androidManifestForAssetModule("com.test.app")) + .build(); + + InvalidBundleException e = + assertThrows( + InvalidBundleException.class, + () -> new AssetsTargetingValidator().validateModule(module)); + + assertThat(e).hasMessageThat().contains("set but empty Country Set targeting"); + } + @Test public void conflictingValuesAndAlternatives_abi() { Assets config = @@ -337,4 +362,34 @@ public void conflictingValuesAndAlternatives_textureCompressionFormat() { + " 'assets/dir' has texture compression format targeting that contains [ASTC] in" + " both."); } + + @Test + public void conflictingValuesAndAlternatives_countrySet() { + Assets config = + assets( + targetedAssetsDirectory( + "assets/dir#countries_latam", + AssetsDirectoryTargeting.newBuilder() + .setCountrySet( + CountrySetTargeting.newBuilder().addValue("latam").addAlternatives("latam")) + .build())); + BundleModule module = + new BundleModuleBuilder("testModule") + .addFile("assets/dir#countries_latam/raw.dat") + .setAssetsConfig(config) + .setManifest(androidManifestForAssetModule("com.test.app")) + .build(); + + InvalidBundleException e = + assertThrows( + InvalidBundleException.class, + () -> new AssetsTargetingValidator().validateModule(module)); + + assertThat(e) + .hasMessageThat() + .contains( + "Expected targeting values and alternatives to be mutually exclusive, but directory" + + " 'assets/dir#countries_latam' has country set targeting that contains [latam] in" + + " both."); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java index e3a02877..7a8f471e 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/BundleConfigValidatorTest.java @@ -209,7 +209,7 @@ public void optimizations_nonTcfDimensionsSuffixStripping_throws() throws Except .hasMessageThat() .contains( "Suffix stripping was enabled for an unsupported dimension. Supported dimensions are:" - + " TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER."); + + " TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER, COUNTRY_SET."); } @Test @@ -320,6 +320,42 @@ public void optimizations_defaultDeviceTier_targeted_unspecified_success() throw new BundleConfigValidator().validateBundle(appBundleBuilder.build()); } + @Test + public void optimizations_defaultCountrySet_targeted_specified_success() throws Exception { + AppBundleBuilder appBundleBuilder = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension(Value.COUNTRY_SET, false, true, "") + .build()) + .addModule( + new BundleModuleBuilder("base") + .addFile("assets/textures#countries_latam/level1.assets") + .addFile("assets/textures#countries_sea/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build()); + + new BundleConfigValidator().validateBundle(appBundleBuilder.build()); + } + + @Test + public void optimizations_defaultCountrySet_targeted_unspecified_success() throws Exception { + AppBundleBuilder appBundleBuilder = + new AppBundleBuilder() + .addModule( + new BundleModuleBuilder("base") + .setManifest(androidManifest("com.test.app")) + .build()) + .addModule( + new BundleModuleBuilder("a") + .addFile("assets/textures#countries_latam/level1.assets") + .addFile("assets/textures#countries_sea/level1.assets") + .setManifest(androidManifest("com.test.app")) + .build()); + + new BundleConfigValidator().validateBundle(appBundleBuilder.build()); + } + @Test public void version_valid_ok() throws Exception { AppBundle appBundle = createAppBundle(BundleConfigBuilder.create().setVersion(CURRENT_VERSION)); 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 new file mode 100644 index 00000000..23b2d64c --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2022 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.validation; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Config.BundleConfig; +import com.android.bundle.Config.Optimizations; +import com.android.bundle.Config.SplitDimension; +import com.android.bundle.Config.SplitDimension.Value; +import com.android.bundle.Config.SplitsConfig; +import com.android.bundle.Config.SuffixStripping; +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.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CountrySetParityValidatorTest { + + @Test + public void validateAllModules_noCountrySets_ok() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img/image.jpg") + .addFile("assets/img/image2.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b").setManifest(androidManifest("com.test.app")).build(); + + new CountrySetParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void validateAllModules_sameCountrySets_ok() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img#countries_latam/image.jpg") + .addFile("assets/img#countries_sea/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")) + .build(); + + new CountrySetParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void validateAllModules_multipleFilesPerCountrySetDirectory_ok() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img#countries_latam/imageA.jpg") + .addFile("assets/img#countries_latam/imageB.jpg") + .addFile("assets/img#countries_sea/image1.jpg") + .addFile("assets/img#countries_sea/image2.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + new CountrySetParityValidator().validateAllModules(ImmutableList.of(moduleA)); + } + + @Test + public void validateAllModules_sameCountrySetsAndNoCountrySet_ok() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img#countries_latam/image.jpg") + .addFile("assets/img#countries_sea/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")) + .build(); + BundleModule moduleC = + new BundleModuleBuilder("c").setManifest(androidManifest("com.test.app")).build(); + + new CountrySetParityValidator().validateAllModules(ImmutableList.of(moduleA, moduleB, moduleC)); + } + + @Test + public void validateAllModules_differentCountrySetsInDifferentModules_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img#countries_latam/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")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new CountrySetParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB))); + + assertThat(exception) + .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)."); + } + + @Test + public void sameCountrySetsInDifferentModules_differentFallback_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")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new CountrySetParityValidator() + .validateAllModules(ImmutableList.of(moduleA, moduleB))); + + 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)."); + } + + @Test + public void validateBundle_moduleWithFallbackCountrySet_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") + .setManifest(androidManifest("com.test.app")) + .build(); + AppBundle appBundle = + AppBundle.buildFromModules( + ImmutableList.of(moduleA), + BundleConfig.newBuilder() + .setOptimizations( + Optimizations.newBuilder() + .setSplitsConfig( + SplitsConfig.newBuilder() + .addSplitDimension( + SplitDimension.newBuilder() + .setValue(Value.COUNTRY_SET) + .setSuffixStripping( + SuffixStripping.newBuilder().setDefaultSuffix(""))))) + .build(), + BundleMetadata.builder().build()); + + new CountrySetParityValidator().validateBundle(appBundle); + } + + @Test + public void validateBundle_moduleWithoutFallbackCountrySet_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#countries_latam/image.jpg") + .addFile("assets/img1#countries_sea/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + AppBundle appBundle = + AppBundle.buildFromModules( + ImmutableList.of(moduleA), + BundleConfig.newBuilder() + .setOptimizations( + Optimizations.newBuilder() + .setSplitsConfig( + SplitsConfig.newBuilder() + .addSplitDimension( + SplitDimension.newBuilder() + .setValue(Value.COUNTRY_SET) + .setSuffixStripping( + SuffixStripping.newBuilder().setDefaultSuffix(""))))) + .build(), + BundleMetadata.builder().build()); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new CountrySetParityValidator().validateBundle(appBundle)); + + 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."); + } + + @Test + public void validateBundle_moduleWithDefaultCountrySet_succeeds() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#countries_latam/image.jpg") + .addFile("assets/img1#countries_sea/image.jpg") + .addFile("assets/img2#countries_latam/image.jpg") + .addFile("assets/img2#countries_sea/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + AppBundle appBundle = + AppBundle.buildFromModules( + ImmutableList.of(moduleA), + BundleConfig.newBuilder() + .setOptimizations( + Optimizations.newBuilder() + .setSplitsConfig( + SplitsConfig.newBuilder() + .addSplitDimension( + SplitDimension.newBuilder() + .setValue(Value.COUNTRY_SET) + .setSuffixStripping( + SuffixStripping.newBuilder().setDefaultSuffix("sea"))))) + .build(), + BundleMetadata.builder().build()); + + new CountrySetParityValidator().validateBundle(appBundle); + } + + @Test + public void validateBundle_moduleWithoutDefaultCountrySet_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#countries_latam/image.jpg") + .addFile("assets/img1#countries_sea/image.jpg") + .addFile("assets/img2#countries_latam/image.jpg") + .addFile("assets/img2#countries_sea/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + AppBundle appBundle = + AppBundle.buildFromModules( + ImmutableList.of(moduleA), + BundleConfig.newBuilder() + .setOptimizations( + Optimizations.newBuilder() + .setSplitsConfig( + SplitsConfig.newBuilder() + .addSplitDimension( + SplitDimension.newBuilder() + .setValue(Value.COUNTRY_SET) + .setSuffixStripping( + SuffixStripping.newBuilder() + .setDefaultSuffix("europe"))))) + .build(), + BundleMetadata.builder().build()); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new CountrySetParityValidator().validateBundle(appBundle)); + + assertThat(exception) + .hasMessageThat() + .contains( + "When a standalone or universal APK is built, the country set folders corresponding to" + + " country set 'europe' 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."); + } + + @Test + public void validateBundle_modulesWithAndWithoutCountrySet_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") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/img2/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + AppBundle appBundle = + AppBundle.buildFromModules( + ImmutableList.of(moduleA, moduleB), + BundleConfig.newBuilder() + .setOptimizations( + Optimizations.newBuilder() + .setSplitsConfig( + SplitsConfig.newBuilder() + .addSplitDimension( + SplitDimension.newBuilder() + .setValue(Value.COUNTRY_SET) + .setSuffixStripping( + SuffixStripping.newBuilder() + .setDefaultSuffix("latam"))))) + .build(), + BundleMetadata.builder().build()); + + new CountrySetParityValidator().validateBundle(appBundle); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidatorTest.java index a8bc16cd..87e7af08 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierConfigValidatorTest.java @@ -24,6 +24,7 @@ import com.android.bundle.DeviceTier; import com.android.bundle.DeviceTierConfig; import com.android.bundle.DeviceTierSet; +import com.android.bundle.UserCountrySet; import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,11 +34,12 @@ public class DeviceTierConfigValidatorTest { @Test - public void noGroups_throws() { + public void noGroups_noCountrySet_throws() { DeviceTierConfig deviceTierConfig = DeviceTierConfig.getDefaultInstance(); assertCommandExecutionExceptionIsThrownAndHasMessage( - deviceTierConfig, "The device tier config must contain at least one group."); + deviceTierConfig, + "The device tier config must contain at least one group or user country set."); } @Test @@ -49,6 +51,81 @@ public void emptyGroupName_throws() { deviceTierConfig, "Device groups must specify a name."); } + @Test + public void emptyCountrySetName_throws() { + DeviceTierConfig deviceTierConfig = + DeviceTierConfig.newBuilder() + .addUserCountrySets(UserCountrySet.getDefaultInstance()) + .build(); + + assertCommandExecutionExceptionIsThrownAndHasMessage( + deviceTierConfig, "Country Sets must specify a name."); + } + + @Test + public void emptyCountryCodesList_throws() { + DeviceTierConfig deviceTierConfig = + DeviceTierConfig.newBuilder() + .addUserCountrySets(UserCountrySet.newBuilder().setName("latam")) + .build(); + + assertCommandExecutionExceptionIsThrownAndHasMessage( + deviceTierConfig, "Country set 'latam' must specify at least one country code."); + } + + @Test + public void duplicateCountrySetNames_throws() { + DeviceTierConfig deviceTierConfig = + DeviceTierConfig.newBuilder() + .addUserCountrySets(UserCountrySet.newBuilder().setName("latam").addCountryCodes("AR")) + .addUserCountrySets(UserCountrySet.newBuilder().setName("latam").addCountryCodes("BR")) + .build(); + + assertCommandExecutionExceptionIsThrownAndHasMessage( + deviceTierConfig, + "Country set names should be unique. Found multiple country sets with these names:" + + " [latam]."); + } + + @Test + public void duplicateCountryCodes_throws() { + DeviceTierConfig deviceTierConfig = + DeviceTierConfig.newBuilder() + .addUserCountrySets(UserCountrySet.newBuilder().setName("sea").addCountryCodes("AR")) + .addUserCountrySets(UserCountrySet.newBuilder().setName("latam").addCountryCodes("AR")) + .build(); + + assertCommandExecutionExceptionIsThrownAndHasMessage( + deviceTierConfig, + "A country code can belong to only one country set. Found multiple occurrences of these" + + " country codes: [AR]."); + } + + @Test + public void invalidCountrySetName_throws() { + DeviceTierConfig deviceTierConfig = + DeviceTierConfig.newBuilder() + .addUserCountrySets( + UserCountrySet.newBuilder().setName("latam#$%").addCountryCodes("AR")) + .build(); + + assertCommandExecutionExceptionIsThrownAndHasMessage( + deviceTierConfig, + "Country set name should match the regex '^[a-zA-Z][a-zA-Z0-9_]*$', but found 'latam#$%'."); + } + + @Test + public void invalidCountryCode_throws() { + DeviceTierConfig deviceTierConfig = + DeviceTierConfig.newBuilder() + .addUserCountrySets( + UserCountrySet.newBuilder().setName("latam").addCountryCodes("brazil")) + .build(); + + assertCommandExecutionExceptionIsThrownAndHasMessage( + deviceTierConfig, "Country code should match the regex '^[A-Z]{2}$', but found 'brazil'."); + } + @Test public void groupWithoutSelector_throws() { String groupName = "groupWithoutSelector"; diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_device_tiers.json b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_device_tiers.json new file mode 100644 index 00000000..57800f38 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_device_tiers.json @@ -0,0 +1,63 @@ +{ + "device_groups": [ + { + "name": "high", + "device_selectors": [ + { + "device_ram": { + "min_bytes": 7516192768 + }, + "included_device_ids": [ + { + "build_brand": "google", + "build_device": "flame" + } + ] + } + ] + }, + { + "name": "medium", + "device_selectors": [ + { + "device_ram": { + "min_bytes": 4294967296, + "max_bytes": 7516192768 + } + } + ] + } + ], + "device_tier_set": { + "device_tiers": [ + { + "level": 1, + "device_group_names": [ + "medium" + ] + }, + { + "level": 2, + "device_group_names": [ + "high" + ] + } + ] + }, + "user_country_sets": [ + { + "name": "latam", + "country_codes": [ + "AR", + "BR" + ] + }, + { + "name": "sea", + "country_codes": [ + "VN", + "TW" + ] + } + ] +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_device_tiers_expected_output.txt b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_device_tiers_expected_output.txt new file mode 100644 index 00000000..8e467450 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_device_tiers_expected_output.txt @@ -0,0 +1,59 @@ +Group 'high': + ( + RAM >= 7.00 GB + AND + device IN ('google flame') + ) + +Group 'medium': + ( + 4.00 GB <= RAM < 7.00 GB + ) + +Tier 2: + ( + ( + RAM >= 7.00 GB + AND + device IN ('google flame') + ) + ) + +Tier 1: + ( + ( + 4.00 GB <= RAM < 7.00 GB + ) + ) AND NOT ( + ( + RAM >= 7.00 GB + AND + device IN ('google flame') + ) + ) + +Tier 0 (default): + NOT ( + ( + 4.00 GB <= RAM < 7.00 GB + ) OR ( + RAM >= 7.00 GB + AND + device IN ('google flame') + ) + ) + +Country set 'latam': + ( + Country Codes: [ + AR, BR + ] + ) + +Country set 'sea': + ( + Country Codes: [ + VN, TW + ] + ) + diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_multiple_groups_and_tiers.json b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_multiple_groups_and_tiers.json new file mode 100644 index 00000000..4033e76b --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_multiple_groups_and_tiers.json @@ -0,0 +1,57 @@ +{ + "device_groups": [ + { + "name": "high", + "device_selectors": [ + { + "device_ram": { + "min_bytes": 7516192768 + } + } + ] + }, + { + "name": "medium", + "device_selectors": [ + { + "device_ram": { + "min_bytes": 4294967296, + "max_bytes": 7516192768 + } + } + ] + } + ], + "device_tier_set": { + "device_tiers": [ + { + "level": 1, + "device_group_names": [ + "medium" + ] + }, + { + "level": 2, + "device_group_names": [ + "high" + ] + } + ] + }, + "user_country_sets": [ + { + "name": "latam", + "country_codes": [ + "AR", + "BR" + ] + }, + { + "name": "sea", + "country_codes": [ + "VN", + "TW" + ] + } + ] +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_multiple_groups_and_tiers_evaluation.txt b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_multiple_groups_and_tiers_evaluation.txt new file mode 100644 index 00000000..35ca7dc3 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/country_sets_with_multiple_groups_and_tiers_evaluation.txt @@ -0,0 +1,3 @@ +Tier: 2 +Groups: 'high' +Country Set: 'latam' diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets.json b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets.json new file mode 100644 index 00000000..9cfcb980 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets.json @@ -0,0 +1,263 @@ +{ + "user_country_sets": [ + { + "name": "latam", + "country_codes": [ + "AR", + "BR" + ] + }, + { + "name": "sea", + "country_codes": [ + "AA", + "AB", + "AC", + "AD", + "AE", + "AF", + "AG", + "AH", + "AI", + "AJ", + "AK", + "AL", + "AM", + "AN", + "AO", + "AP", + "AQ", + "AS", + "AT", + "AU", + "AV", + "AW", + "AX", + "AY", + "AZ", + "BA", + "BB", + "BC", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BK", + "BL", + "BM", + "BN", + "BO", + "BP", + "BQ", + "BS", + "BT", + "BU", + "BV", + "BW", + "BX", + "BY", + "BZ", + "CA", + "CB", + "CC", + "CD", + "CE", + "CF", + "CG", + "CH", + "CI", + "CJ", + "CK", + "CL", + "CM", + "CN", + "CO", + "CP", + "CQ", + "CR", + "CS", + "CT", + "CU", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DA", + "DB", + "DC", + "DD", + "DE", + "DF", + "DG", + "DH", + "DI", + "DJ", + "DK", + "DL", + "DM", + "DN", + "DO", + "DP", + "DQ", + "DR", + "DS", + "DT", + "DU", + "DV", + "DW", + "DX", + "DY", + "DZ", + "EA", + "EB", + "EC", + "ED", + "EE", + "EF", + "EG", + "EH", + "EI", + "EJ", + "EK", + "EL", + "EM", + "EN", + "EO", + "EP", + "EQ", + "ER", + "ES", + "ET", + "EU", + "EV", + "EW", + "EX", + "EY", + "EZ", + "FA", + "FB", + "FC", + "FD", + "FE", + "FF", + "FG", + "FH", + "FI", + "FJ", + "FK", + "FL", + "FM", + "FN", + "FO", + "FP", + "FQ", + "FR", + "FS", + "FT", + "FU", + "FV", + "FW", + "FX", + "FY", + "FZ", + "GA", + "GB", + "GC", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GJ", + "GK", + "GL", + "GM", + "GN", + "GO", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GV", + "GW", + "GX", + "GY", + "GZ", + "HA", + "HB", + "HC", + "HD", + "HE", + "HF", + "HG", + "HH", + "HI", + "HJ", + "HK", + "HL", + "HM", + "HN", + "HO", + "HP", + "HQ", + "HR", + "HS", + "HT", + "HU", + "HV", + "HW", + "HX", + "HY", + "HZ", + "IA", + "IB", + "IC", + "ID", + "IE", + "IF", + "IG", + "IH", + "II", + "IJ", + "IK", + "IL", + "IM", + "IN", + "IO", + "IP", + "IQ", + "IR", + "IS", + "IT", + "IU", + "IV", + "IW", + "IX", + "IY", + "IZ", + "JA", + "JB", + "JC", + "JD", + "JE", + "JF", + "JG", + "JH", + "JI", + "JJ", + "JK", + "JL", + "JM", + "JN", + "JO" + ] + } + ] +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_evaluation.txt b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_evaluation.txt new file mode 100644 index 00000000..d71b742e --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_evaluation.txt @@ -0,0 +1,3 @@ +Tier: 0 (default) +Groups: +Country Set: 'latam' diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_expected_output.txt b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_expected_output.txt new file mode 100644 index 00000000..e434e46c --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_expected_output.txt @@ -0,0 +1,38 @@ +Country set 'latam': + ( + Country Codes: [ + AR, BR + ] + ) + +Country set 'sea': + ( + Country Codes: [ + AA, AB, AC, AD, AE, AF, AG, AH, AI, AJ + AK, AL, AM, AN, AO, AP, AQ, AS, AT, AU + AV, AW, AX, AY, AZ, BA, BB, BC, BD, BE + BF, BG, BH, BI, BJ, BK, BL, BM, BN, BO + BP, BQ, BS, BT, BU, BV, BW, BX, BY, BZ + CA, CB, CC, CD, CE, CF, CG, CH, CI, CJ + CK, CL, CM, CN, CO, CP, CQ, CR, CS, CT + CU, CV, CW, CX, CY, CZ, DA, DB, DC, DD + DE, DF, DG, DH, DI, DJ, DK, DL, DM, DN + DO, DP, DQ, DR, DS, DT, DU, DV, DW, DX + DY, DZ, EA, EB, EC, ED, EE, EF, EG, EH + EI, EJ, EK, EL, EM, EN, EO, EP, EQ, ER + ES, ET, EU, EV, EW, EX, EY, EZ, FA, FB + FC, FD, FE, FF, FG, FH, FI, FJ, FK, FL + FM, FN, FO, FP, FQ, FR, FS, FT, FU, FV + FW, FX, FY, FZ, GA, GB, GC, GD, GE, GF + GG, GH, GI, GJ, GK, GL, GM, GN, GO, GP + GQ, GR, GS, GT, GU, GV, GW, GX, GY, GZ + HA, HB, HC, HD, HE, HF, HG, HH, HI, HJ + HK, HL, HM, HN, HO, HP, HQ, HR, HS, HT + HU, HV, HW, HX, HY, HZ, IA, IB, IC, ID + IE, IF, IG, IH, II, IJ, IK, IL, IM, IN + IO, IP, IQ, IR, IS, IT, IU, IV, IW, IX + IY, IZ, JA, JB, JC, JD, JE, JF, JG, JH + JI, JJ, JK, JL, JM, JN, JO + ] + ) + diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_restofworld_country_code_evaluation.txt b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_restofworld_country_code_evaluation.txt new file mode 100644 index 00000000..f1831d6d --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/device_targeting_config/only_country_sets_restofworld_country_code_evaluation.txt @@ -0,0 +1,3 @@ +Tier: 0 (default) +Groups: +Country Set: ''