diff --git a/README.md b/README.md index 19b16851..1adfc334 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.14.0](https://github.com/google/bundletool/releases) +Latest release: [1.14.1](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index 3797761b..8d9887c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.14.0 +release_version = 1.14.1 diff --git a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java index 5acea4c2..d939877c 100644 --- a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedAndroidManifestUtils.java @@ -90,7 +90,8 @@ public final class ArchivedAndroidManifestUtils { AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME, AndroidManifest.PERMISSION_TREE_ELEMENT_NAME); - public static AndroidManifest createArchivedManifest(AndroidManifest manifest) { + public static AndroidManifest createArchivedManifest( + AndroidManifest manifest, boolean createDifferentThemesForTvAndPhone) { checkNotNull(manifest); ManifestEditor editor = @@ -130,7 +131,27 @@ public static AndroidManifest createArchivedManifest(AndroidManifest manifest) { CHILDREN_ELEMENTS_TO_KEEP.forEach( elementName -> editor.copyChildrenElements(manifest, elementName)); - editor.addActivity(createReactivateActivity(manifest)); + if (createDifferentThemesForTvAndPhone) { + if (manifest.hasMainActivity()) { + editor.addActivity( + createReactivateActivity( + IntentFilter.builder() + .addActionName(MAIN_ACTION_NAME) + .addCategoryName(LAUNCHER_CATEGORY_NAME) + .build())); + } + if (manifest.hasMainTvActivity()) { + editor.addActivity( + createReactivateActivity( + IntentFilter.builder() + .addActionName(MAIN_ACTION_NAME) + .addCategoryName(LEANBACK_LAUNCHER_CATEGORY_NAME) + .build())); + } + } else { + editor.addActivity(createReactivateActivity(manifest)); + } + editor.addReceiver(createUpdateBroadcastReceiver()); addTvSupportIfRequired(editor, manifest); @@ -145,7 +166,9 @@ private static XmlProtoNode createMinimalManifestTag() { } public static AndroidManifest updateArchivedIconsAndTheme( - AndroidManifest manifest, ImmutableMap resourceNameToIdMap) { + AndroidManifest manifest, + ImmutableMap resourceNameToIdMap, + boolean createDifferentThemesForTvAndPhone) { ManifestEditor archivedManifestEditor = manifest.toEditor(); if (manifest.getIconAttribute().isPresent() @@ -159,13 +182,40 @@ public static AndroidManifest updateArchivedIconsAndTheme( resourceNameToIdMap.get(ARCHIVED_ROUND_ICON_DRAWABLE_NAME)); } - archivedManifestEditor.setActivityTheme( - REACTIVATE_ACTIVITY_NAME, - resourceNameToIdMap.getOrDefault(ArchivedResourcesHelper.ARCHIVED_TV_THEME_NAME, 0)); + if (createDifferentThemesForTvAndPhone) { + if (manifest.hasMainActivity()) { + archivedManifestEditor.setActivityTheme( + REACTIVATE_ACTIVITY_NAME, + LAUNCHER_CATEGORY_NAME, + HOLO_LIGHT_NO_ACTION_BAR_FULSCREEN_THEME_RESOURCE_ID); + } + if (manifest.hasMainTvActivity()) { + archivedManifestEditor.setActivityTheme( + REACTIVATE_ACTIVITY_NAME, + LEANBACK_LAUNCHER_CATEGORY_NAME, + checkNotNull(resourceNameToIdMap.get(ArchivedResourcesHelper.ARCHIVED_TV_THEME_NAME)) + .intValue()); + } + } else { + archivedManifestEditor.setActivityTheme( + REACTIVATE_ACTIVITY_NAME, + resourceNameToIdMap.getOrDefault(ArchivedResourcesHelper.ARCHIVED_TV_THEME_NAME, 0)); + } return archivedManifestEditor.save(); } + private static Activity createReactivateActivity(IntentFilter intentFilter) { + return Activity.builder() + .setName(REACTIVATE_ACTIVITY_NAME) + .setExported(true) + .setExcludeFromRecents(true) + .setStateNotNeeded(true) + .setNoHistory(true) + .setIntentFilter(intentFilter) + .build(); + } + private static Activity createReactivateActivity(AndroidManifest manifest) { IntentFilter.Builder intentFilterBuilder = IntentFilter.builder().addActionName(MAIN_ACTION_NAME); diff --git a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java index fd9cd40b..6ebe14d5 100644 --- a/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/archive/ArchivedApksGenerator.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.android.aapt.Resources.ResourceTable; +import com.android.tools.build.bundletool.commands.BuildApksModule.DifferentThemesForTvAndPhone; import com.android.tools.build.bundletool.io.ResourceReader; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.AppBundle; @@ -48,11 +49,13 @@ public final class ArchivedApksGenerator { private final ResourceReader resourceReader; private final ArchivedResourcesHelper archivedResourcesHelper; + private final boolean createDifferentThemesForTvAndPhone; @Inject - ArchivedApksGenerator() { + ArchivedApksGenerator(@DifferentThemesForTvAndPhone boolean createDifferentThemesForTvAndPhone) { resourceReader = new ResourceReader(); archivedResourcesHelper = new ArchivedResourcesHelper(resourceReader); + this.createDifferentThemesForTvAndPhone = createDifferentThemesForTvAndPhone; } public ModuleSplit generateArchivedApk( @@ -62,7 +65,8 @@ public ModuleSplit generateArchivedApk( BundleModule baseModule = appBundle.getBaseModule(); AndroidManifest archivedManifest = - ArchivedAndroidManifestUtils.createArchivedManifest(baseModule.getAndroidManifest()); + ArchivedAndroidManifestUtils.createArchivedManifest( + baseModule.getAndroidManifest(), createDifferentThemesForTvAndPhone); ResourceTable archivedResourceTable = getArchivedResourceTable(appBundle, baseModule, archivedManifest); @@ -87,7 +91,7 @@ public ModuleSplit generateArchivedApk( archivedManifest = ArchivedAndroidManifestUtils.updateArchivedIconsAndTheme( - archivedManifest, extraResourceNameToIdMap); + archivedManifest, extraResourceNameToIdMap, createDifferentThemesForTvAndPhone); ModuleSplit moduleSplit = ModuleSplit.forArchive( diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java index 25da71ed..84484936 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksManager.java @@ -25,6 +25,7 @@ import com.android.bundle.Commands.LocalTestingInfo; import com.android.bundle.Config.BundleConfig; import com.android.bundle.Config.ResourceOptimizations.SparseEncoding; +import com.android.bundle.Config.StandaloneConfig.FeatureModulesMode; import com.android.bundle.Devices.DeviceSpec; import com.android.tools.build.bundletool.archive.ArchivedApksGenerator; import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; @@ -371,6 +372,14 @@ private ApkGenerationConfiguration getAssetSliceGenerationConfiguration() { } private ImmutableList modulesToFuse(ImmutableList modules) { + if (appBundle + .getBundleConfig() + .getOptimizations() + .getStandaloneConfig() + .getFeatureModulesMode() + .equals(FeatureModulesMode.SEPARATE_FEATURE_MODULES)) { + return modules; + } return modules.stream() .filter(BundleModule::isIncludedInFusing) .filter( 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 d847b3b2..619e4cd0 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 @@ -172,6 +172,15 @@ static Optional provideLocalRuntimeEnabl return command.getLocalDeploymentRuntimeEnabledSdkConfig(); } + @CommandScoped + @Provides + @DifferentThemesForTvAndPhone + static boolean provideDifferentThemesForTvAndPhone(BuildApksCommand command) { + @SuppressWarnings("unused") + boolean differentThemesForTvAndPhone = false; + return differentThemesForTvAndPhone; + } + /** * Qualifying annotation of an {@code Optional} for the first variant number to use when * numbering the generated variants. @@ -198,5 +207,13 @@ static Optional provideLocalRuntimeEnabl @Retention(RUNTIME) public @interface ApkSigningConfigProvider {} + /** + * Qualifying annotation of a {@code boolean} to enable usage of different themes for tv and + * phone. + */ + @Qualifier + @Retention(RUNTIME) + public @interface DifferentThemesForTvAndPhone {} + private BuildApksModule() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/device/GlExtensionsParser.java b/src/main/java/com/android/tools/build/bundletool/device/GlExtensionsParser.java index 62f1055e..629374bc 100644 --- a/src/main/java/com/android/tools/build/bundletool/device/GlExtensionsParser.java +++ b/src/main/java/com/android/tools/build/bundletool/device/GlExtensionsParser.java @@ -16,7 +16,6 @@ package com.android.tools.build.bundletool.device; -import com.android.tools.build.bundletool.model.exceptions.AdbOutputParseException; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; @@ -66,10 +65,9 @@ public ImmutableList parse(ImmutableList dumpSysOutput) { } if (!parsingState.equals(ParsingState.FOUND_GL_EXTENSIONS)) { - throw AdbOutputParseException.builder() - .withInternalMessage( - "Unexpected output of 'dumpsys SurfaceFlinger' command: no GL extensions found.") - .build(); + System.out.println( + "WARNING: Unexpected output of 'dumpsys SurfaceFlinger' command: no GL extensions" + + " found."); } return glExtensions.build(); diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java b/src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java index 5c80d46b..66e89f38 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkDescriptionHelper.java @@ -54,6 +54,7 @@ static ApkDescription createApkDescription( } break; case STANDALONE: + case STANDALONE_FEATURE_MODULE: if (split.isApex()) { resultBuilder.setApexApkMetadata(createApexApkMetadata(split)); } else { diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java b/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java index 55c6051d..9b4a1049 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkPathManager.java @@ -91,6 +91,11 @@ public ZipPath getApkPath(ModuleSplit moduleSplit) { directory = ZipPath.create("standalones"); apkFileName = buildName("standalone", targetingSuffix); break; + case STANDALONE_FEATURE_MODULE: + directory = ZipPath.create("standalones"); + apkFileName = + buildName(moduleSplit.isBaseModuleSplit() ? "standalone" : moduleName, targetingSuffix); + break; case SYSTEM: if (moduleSplit.isBaseModuleSplit() && moduleSplit.isMasterSplit()) { directory = ZipPath.create("system"); 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 bd15fce4..42d16b97 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 @@ -51,6 +51,7 @@ import com.android.bundle.Devices.DeviceSpec; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode; +import com.android.tools.build.bundletool.commands.BuildApksModule.ApkSigningConfigProvider; import com.android.tools.build.bundletool.commands.BuildApksModule.FirstVariantNumber; import com.android.tools.build.bundletool.device.ApkMatcher; import com.android.tools.build.bundletool.model.AndroidManifest; @@ -68,6 +69,7 @@ import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.SigningConfigurationProvider; import com.android.tools.build.bundletool.model.VariantKey; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.version.BundleToolVersion; @@ -101,6 +103,7 @@ public class ApkSerializerManager { private final ApkPathManager apkPathManager; private final ApkOptimizations apkOptimizations; private final ApkSerializer apkSerializer; + private final Optional signingConfigProvider; @Inject public ApkSerializerManager( @@ -110,7 +113,8 @@ public ApkSerializerManager( ApkBuildMode apkBuildMode, ApkPathManager apkPathManager, ApkOptimizations apkOptimizations, - ApkSerializer apkSerializer) { + ApkSerializer apkSerializer, + @ApkSigningConfigProvider Optional signingConfigProvider) { this.bundle = bundle; this.apkModifier = apkModifier.orElse(ApkModifier.NO_OP); this.firstVariantNumber = firstVariantNumber.orElse(0); @@ -118,6 +122,7 @@ public ApkSerializerManager( this.apkPathManager = apkPathManager; this.apkOptimizations = apkOptimizations; this.apkSerializer = apkSerializer; + this.signingConfigProvider = signingConfigProvider; } /** Serialize App Bundle APKs. */ @@ -483,10 +488,21 @@ private ModuleSplit modifyApk(ModuleSplit moduleSplit, int variantNumber) { .build(); } - private static ModuleSplit clearVariantTargeting(ModuleSplit moduleSplit) { - return moduleSplit.toBuilder() - .setVariantTargeting(VariantTargeting.getDefaultInstance()) - .build(); + private ModuleSplit clearVariantTargeting(ModuleSplit moduleSplit) { + VariantTargeting.Builder variantTargeting = VariantTargeting.newBuilder(); + boolean hasRestrictedV3SigningConfig = + signingConfigProvider + .map(SigningConfigurationProvider::hasRestrictedV3SigningConfig) + .orElse(false); + // If the signing config includes signing with rotated keys using V3 signature scheme, and it is + // restricted to specific Android SDK versions, then the de-duplication of generated splits must + // account for variant SDK version targeting when comparing splits. + if (hasRestrictedV3SigningConfig + && moduleSplit.getVariantTargeting().hasSdkVersionTargeting()) { + variantTargeting.setSdkVersionTargeting( + moduleSplit.getVariantTargeting().getSdkVersionTargeting()); + } + return moduleSplit.toBuilder().setVariantTargeting(variantTargeting.build()).build(); } private static AssetModulesInfo getAssetModulesInfo(AssetModulesConfig assetModulesConfig) { diff --git a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java index 36fa8267..0d275ce4 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java @@ -95,6 +95,7 @@ public abstract class AndroidManifest { public static final String REMOVABLE_ELEMENT_NAME = "removable"; public static final String FUSING_ELEMENT_NAME = "fusing"; public static final String STYLE_ELEMENT_NAME = "style"; + public static final String MAIN_ACTION_ELEMENT_NAME = "action"; public static final String DEBUGGABLE_ATTRIBUTE_NAME = "debuggable"; public static final String EXTRACT_NATIVE_LIBS_ATTRIBUTE_NAME = "extractNativeLibs"; @@ -135,12 +136,14 @@ public abstract class AndroidManifest { public static final String LOCALE_CONFIG_ATTRIBUTE_NAME = "localeConfig"; public static final String SDK_LIBRARY_ELEMENT_NAME = "sdk-library"; public static final String SDK_VERSION_MAJOR_ATTRIBUTE_NAME = "versionMajor"; + public static final String ISOLATED_SPLITS_ATTRIBUTE_NAME = "isolatedSplits"; public static final String SDK_PATCH_VERSION_ATTRIBUTE_NAME = "com.android.vending.sdk.version.patch"; public static final String SDK_PROVIDER_CLASS_NAME_ATTRIBUTE_NAME = "android.sdksandbox.PROPERTY_SDK_PROVIDER_CLASS_NAME"; public static final String COMPAT_SDK_PROVIDER_CLASS_NAME_ATTRIBUTE_NAME = "android.sdksandbox.PROPERTY_COMPAT_SDK_PROVIDER_CLASS_NAME"; + public static final String REQUIRED_ATTRIBUTE_NAME = "required"; public static final int SDK_SANDBOX_MIN_VERSION = ANDROID_T_API_VERSION; public static final String USES_SDK_LIBRARY_ELEMENT_NAME = "uses-sdk-library"; @@ -913,12 +916,11 @@ public ImmutableList getSdkLibraryElements() { /** * Gets the SDK patch version if it is set in the AndroidManifest. If there are multiple - * elements with android:name={@value SDK_PATCH_VERSION_ATTRIBUTE_NAME}, return the + * elements with android:name={@value SDK_PATCH_VERSION_ATTRIBUTE_NAME}, return the * first one. */ - public Optional getSdkPatchVersionProperty() { - return getPropertyValue(SDK_PATCH_VERSION_ATTRIBUTE_NAME) - .map(XmlProtoAttribute::getValueAsInteger); + public Optional getSdkPatchVersionMetadata() { + return getMetadataValueAsInteger(SDK_PATCH_VERSION_ATTRIBUTE_NAME); } /** diff --git a/src/main/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProvider.java b/src/main/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProvider.java index 691e1267..7784b475 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProvider.java +++ b/src/main/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProvider.java @@ -52,6 +52,12 @@ public ApksigSigningConfiguration getSigningConfiguration(ApkDescription apkDesc return apksigSigningConfig.build(); } + @Override + public boolean hasRestrictedV3SigningConfig() { + return signingConfiguration.getSigningCertificateLineage().isPresent() + && signingConfiguration.getEffectiveMinimumV3RotationApiVersion() > 1; + } + private ImmutableList getSignerConfigs(ApkDescription apkDescription) { ImmutableList.Builder signerConfigs = ImmutableList.builder(); Optional oldestSigner = signingConfiguration.getOldestSigner(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java b/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java index bea5c165..9b4fde22 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java +++ b/src/main/java/com/android/tools/build/bundletool/model/GeneratedApks.java @@ -41,6 +41,8 @@ public abstract class GeneratedApks { public abstract ImmutableList getStandaloneApks(); + public abstract ImmutableList getStandaloneFeatureModuleApks(); + public abstract ImmutableList getSystemApks(); // There is alsways a single archived APK. List type is used for consistency. @@ -50,6 +52,7 @@ public int size() { return getInstantApks().size() + getSplitApks().size() + getStandaloneApks().size() + + getStandaloneFeatureModuleApks().size() + getSystemApks().size() + getArchivedApks().size(); } @@ -57,6 +60,7 @@ public int size() { public Stream getAllApksStream() { return Stream.of( getStandaloneApks(), + getStandaloneFeatureModuleApks(), getInstantApks(), getSplitApks(), getSystemApks(), @@ -75,6 +79,7 @@ public static Builder builder() { .setInstantApks(ImmutableList.of()) .setSplitApks(ImmutableList.of()) .setStandaloneApks(ImmutableList.of()) + .setStandaloneFeatureModuleApks(ImmutableList.of()) .setSystemApks(ImmutableList.of()) .setArchivedApks(ImmutableList.of()); } @@ -87,6 +92,8 @@ public static GeneratedApks fromModuleSplits(ImmutableList moduleSp .setInstantApks(groups.getOrDefault(SplitType.INSTANT, ImmutableList.of())) .setSplitApks(groups.getOrDefault(SplitType.SPLIT, ImmutableList.of())) .setStandaloneApks(groups.getOrDefault(SplitType.STANDALONE, ImmutableList.of())) + .setStandaloneFeatureModuleApks( + groups.getOrDefault(SplitType.STANDALONE_FEATURE_MODULE, ImmutableList.of())) .setSystemApks(groups.getOrDefault(SplitType.SYSTEM, ImmutableList.of())) .setArchivedApks(groups.getOrDefault(SplitType.ARCHIVE, ImmutableList.of())) .build(); @@ -102,6 +109,9 @@ public abstract static class Builder { public abstract Builder setStandaloneApks(ImmutableList standaloneApks); + public abstract Builder setStandaloneFeatureModuleApks( + ImmutableList standaloneApks); + public abstract Builder setSystemApks(ImmutableList systemApks); public abstract Builder setArchivedApks(ImmutableList archivedApks); diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java index 7c555190..1026afba 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java @@ -21,6 +21,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.ALLOW_BACKUP_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.APPLICATION_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.CATEGORY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.CERTIFICATE_DIGEST_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.CERTIFICATE_DIGEST_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.COMPAT_SDK_PROVIDER_CLASS_NAME_ATTRIBUTE_NAME; @@ -34,6 +35,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.ICON_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.INCLUDE_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_TIME_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.INTENT_FILTER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_FEATURE_SPLIT_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_RESOURCE_ID; @@ -78,6 +80,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.VERSION_NAME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.VERSION_NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder.createAndroidAttribute; +import static com.google.common.collect.MoreCollectors.onlyElement; import static com.google.common.collect.MoreCollectors.toOptional; import static java.util.stream.Collectors.joining; @@ -217,7 +220,7 @@ public ManifestEditor addMetaDataBoolean(String key, boolean value) { @CanIgnoreReturnValue public ManifestEditor addMetaDataResourceId(String key, int resourceId) { return addMetaDataValue( - key, createAndroidAttribute("value", RESOURCE_RESOURCE_ID).setValueAsRefId(resourceId)); + key, createAndroidAttribute("resource", RESOURCE_RESOURCE_ID).setValueAsRefId(resourceId)); } @CanIgnoreReturnValue @@ -398,6 +401,53 @@ public ManifestEditor setActivityTheme(String activityName, int themeResId) { return this; } + /** + * Sets the theme of an activity that has an action with the name mainActionCategoryName to the + * given themeResId. This method assumes such activity exists. + */ + @CanIgnoreReturnValue + public ManifestEditor setActivityTheme(String activityName, String categoryName, int themeResId) { + // Find the activity that has intentFilter -> action (with name mainActionCategoryName). + XmlProtoElementBuilder activityElement = + manifestElement + .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) + .getChildrenElements(ACTIVITY_ELEMENT_NAME) + .filter( + activity -> + activity + .getOrCreateAndroidAttribute(NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) + .getValueAsString() + .equals(activityName) + && hasIntentFilterWithCategoryName(activity, categoryName)) + .collect(onlyElement()); + + // set the theme of the activity. + activityElement + .getOrCreateAndroidAttribute(THEME_ATTRIBUTE_NAME, THEME_RESOURCE_ID) + .setValueAsRefId(themeResId); + return this; + } + + private boolean hasIntentFilterWithCategoryName( + XmlProtoElementBuilder element, String categoryName) { + return element + .getChildrenElements(INTENT_FILTER_ELEMENT_NAME) + .filter( + intentFilter -> + intentFilter + .getChildrenElements(CATEGORY_ELEMENT_NAME) + .filter(categoryElement -> hasName(categoryElement, categoryName)) + .findAny() + .isPresent()) + .findAny() + .isPresent(); + } + + private boolean hasName(XmlProtoElementBuilder element, String name) { + Optional action = element.getAndroidAttribute(NAME_RESOURCE_ID); + return action.isPresent() && action.get().getValueAsString().equals(name); + } + @CanIgnoreReturnValue public ManifestEditor addReceiver(Receiver receiver) { manifestElement @@ -454,13 +504,13 @@ public ManifestEditor setSdkLibraryElement(String sdkPackageName, int versionMaj return this; } - /** Creates a element and populates it with SDK patch version. */ + /** Creates a element and populates it with SDK patch version. */ @CanIgnoreReturnValue - public ManifestEditor setSdkPatchVersionProperty(int patchVersion) { + public ManifestEditor setSdkPatchVersionMetadata(int patchVersion) { manifestElement .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(PROPERTY_ELEMENT_NAME) + XmlProtoElementBuilder.create(META_DATA_ELEMENT_NAME) .addAttribute( createAndroidAttribute(NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) .setValueAsString(SDK_PATCH_VERSION_ATTRIBUTE_NAME)) 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 fad761a2..772c27fb 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 @@ -97,6 +97,7 @@ public enum SplitType { INSTANT, ASSET_SLICE, ARCHIVE, + STANDALONE_FEATURE_MODULE } /** @@ -377,10 +378,10 @@ public ModuleSplit overrideMinSdkVersionForSdkSandbox() { return this; } - /** Writes the SDK Patch version to a new element. */ + /** Writes the SDK Patch version to a new element. */ public ModuleSplit writePatchVersion(int patchVersion) { AndroidManifest apkManifest = - getAndroidManifest().toEditor().setSdkPatchVersionProperty(patchVersion).save(); + getAndroidManifest().toEditor().setSdkPatchVersionMetadata(patchVersion).save(); return toBuilder().setAndroidManifest(apkManifest).build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/RuntimeEnabledSdkVersionEncoder.java b/src/main/java/com/android/tools/build/bundletool/model/RuntimeEnabledSdkVersionEncoder.java index 479f837a..d30852f7 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/RuntimeEnabledSdkVersionEncoder.java +++ b/src/main/java/com/android/tools/build/bundletool/model/RuntimeEnabledSdkVersionEncoder.java @@ -39,6 +39,9 @@ public final class RuntimeEnabledSdkVersionEncoder { /** Each major version can have 10,000 minor versions with values between [0, 10,000). */ public static final int VERSION_MINOR_MAX_VALUE = VERSION_MAJOR_OFFSET - 1; + /** Maximum allowed value for SDK patch version. */ + public static final int SDK_PATCH_VERSION_MAX_VALUE = 9999; + /** * Encodes SDK major and minor version into a single {@code long}, to be used as the value of * android:versionMajor attribute of and tags of the APK diff --git a/src/main/java/com/android/tools/build/bundletool/model/SigningConfigurationProvider.java b/src/main/java/com/android/tools/build/bundletool/model/SigningConfigurationProvider.java index 2ecfd891..f807af16 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/SigningConfigurationProvider.java +++ b/src/main/java/com/android/tools/build/bundletool/model/SigningConfigurationProvider.java @@ -17,9 +17,10 @@ package com.android.tools.build.bundletool.model; import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getMinSdk; -import static java.lang.Math.max; +import static com.google.common.primitives.Ints.max; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.VariantTargeting; import com.google.auto.value.AutoValue; /** Allows clients to provide a {@link SigningConfiguration} for each generated APK. */ @@ -28,6 +29,12 @@ public interface SigningConfigurationProvider { /** Invoked before signing each generated {@link ModuleSplit}. */ ApksigSigningConfiguration getSigningConfiguration(ApkDescription apkDescription); + /** + * Checks if the signing configuration provided includes signing with rotated keys using V3 + * signature restricted for specifc Android SDK versions. + */ + boolean hasRestrictedV3SigningConfig(); + /** Description of an APK generated by bundletool. */ @AutoValue public abstract class ApkDescription { @@ -38,13 +45,19 @@ public abstract class ApkDescription { /** Targeting of the APK. */ public abstract ApkTargeting getApkTargeting(); + /** Targeting of the variant. */ + public abstract VariantTargeting getVariantTargeting(); + /** * Minimum platform API version that the APK will be installed on. This is derived as the - * highest version out of the minSdkVersion (from Android manifest), and the ApkTargeting + * highest version out of the minSdkVersion (from Android manifest), the ApkTargeting, and the + * VariantTargeting. */ public int getMinSdkVersionTargeting() { int minApkTargetingSdkVersion = getMinSdk(getApkTargeting().getSdkVersionTargeting()); - return max(getMinSdkVersionFromManifest(), minApkTargetingSdkVersion); + int minVariantTargetingSdkVersion = getMinSdk(getVariantTargeting().getSdkVersionTargeting()); + return max( + getMinSdkVersionFromManifest(), minApkTargetingSdkVersion, minVariantTargetingSdkVersion); } public static ApkDescription fromModuleSplit(ModuleSplit moduleSplit) { @@ -52,6 +65,7 @@ public static ApkDescription fromModuleSplit(ModuleSplit moduleSplit) { return builder() .setMinSdkVersionFromManifest(minSdkVersionFromManifest) .setApkTargeting(moduleSplit.getApkTargeting()) + .setVariantTargeting(moduleSplit.getVariantTargeting()) .build(); } @@ -66,6 +80,8 @@ public abstract static class Builder { public abstract Builder setApkTargeting(ApkTargeting apkTargeting); + public abstract Builder setVariantTargeting(VariantTargeting variantTargeting); + public abstract ApkDescription build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java b/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java index 94e28f32..c9cd7b43 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java +++ b/src/main/java/com/android/tools/build/bundletool/model/VariantKey.java @@ -20,6 +20,7 @@ import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.INSTANT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SPLIT; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.STANDALONE; +import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.STANDALONE_FEATURE_MODULE; import static com.android.tools.build.bundletool.model.ModuleSplit.SplitType.SYSTEM; import static com.android.tools.build.bundletool.model.targeting.TargetingComparators.VARIANT_TARGETING_COMPARATOR; import static java.util.Comparator.comparing; @@ -54,7 +55,8 @@ public int compareTo(VariantKey o) { // System APKs never occur with other apk types, its ordering position doesn't matter. return comparing( VariantKey::getSplitType, - Ordering.explicit(INSTANT, STANDALONE, SPLIT, ARCHIVE, SYSTEM)) + Ordering.explicit( + INSTANT, STANDALONE, STANDALONE_FEATURE_MODULE, SPLIT, ARCHIVE, SYSTEM)) .thenComparing(VariantKey::getVariantTargeting, VARIANT_TARGETING_COMPARATOR) .compare(this, o); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/DimensionKeyValue.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/DimensionKeyValue.java new file mode 100644 index 00000000..1118d8a2 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/DimensionKeyValue.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.model.targeting; + +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.google.auto.value.AutoValue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents dimension value used in targeted directory path. For a dimension targeting like + * 'tcf_astc', DimensionKeyValue parses 'tcf' as dimension key and 'astc' as dimension value. + */ +@AutoValue +public abstract class DimensionKeyValue { + private static final Pattern DIMENSION_KEY_VALUE_PATTERN = + Pattern.compile("(?[a-z]+)_(?.+)"); + + public abstract String getDimensionKey(); + + public abstract String getDimensionValue(); + + public static DimensionKeyValue parse(String dimensionValue) { + Matcher matcher = DIMENSION_KEY_VALUE_PATTERN.matcher(dimensionValue); + if (matcher.matches()) { + String key = matcher.group("key"); + return new AutoValue_DimensionKeyValue(key, matcher.group("value")); + } else { + throw InvalidBundleException.builder() + .withUserMessage( + "Cannot tokenize targeted directory segment '%s'." + + " Expecting targeting dimension value in the '_' format.", + dimensionValue) + .build(); + } + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java index 6d0614c6..31beaa76 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectory.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.model.targeting; +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.extractDimensionTargeting; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.MoreCollectors.toOptional; @@ -98,10 +99,16 @@ public String getSubPathBaseName(TargetingDimension dimension) { */ public Optional getTargeting(TargetingDimension dimension) { // We're assuming that dimensions are not duplicated (see checkNoDuplicateDimensions). + return getTargeting() + .flatMap(directoryTargeting -> extractDimensionTargeting(directoryTargeting, dimension)); + } + + /** Returns the value of the targeting for this directory. */ + private Optional getTargeting() { return getPathSegments().stream() - .filter(segment -> segment.getTargetingDimension().equals(Optional.of(dimension))) - .map(segment -> segment.getTargeting()) - .collect(toOptional()); + .filter(segment -> !segment.getTargetingDimensions().isEmpty()) + .findFirst() + .map(TargetedDirectorySegment::getTargeting); } /** @@ -152,9 +159,7 @@ public ZipPath toZipPath() { private Optional getTargetedPathSegmentIndex(TargetingDimension dimension) { // We're assuming that dimensions are not duplicated (see checkNoDuplicateDimensions). return IntStream.range(0, getPathSegments().size()) - .filter( - idx -> - getPathSegments().get(idx).getTargetingDimension().equals(Optional.of(dimension))) + .filter(idx -> getPathSegments().get(idx).getTargetingDimensions().contains(dimension)) .boxed() .collect(toOptional()); } @@ -163,18 +168,18 @@ private static void checkNoDuplicateDimensions( ImmutableList directorySegments, ZipPath directoryPath) { Set coveredDimensions = new HashSet<>(); for (TargetedDirectorySegment targetedDirectorySegment : directorySegments) { - if (targetedDirectorySegment.getTargetingDimension().isPresent()) { - TargetingDimension lastSegmentDimension = - targetedDirectorySegment.getTargetingDimension().get(); - if (coveredDimensions.contains(lastSegmentDimension)) { - throw InvalidBundleException.builder() - .withUserMessage( - "Duplicate targeting dimension '%s' on path '%s'.", - lastSegmentDimension, directoryPath) - .build(); - } - coveredDimensions.add(lastSegmentDimension); - } + ImmutableList segmentTargetingDimensions = + targetedDirectorySegment.getTargetingDimensions(); + segmentTargetingDimensions.forEach( + dimension -> { + if (coveredDimensions.contains(dimension)) { + throw InvalidBundleException.builder() + .withUserMessage( + "Duplicate targeting dimension '%s' on path '%s'.", dimension, directoryPath) + .build(); + } + }); + coveredDimensions.addAll(segmentTargetingDimensions); } } 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 74a78839..8ee9f151 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 @@ -16,8 +16,9 @@ package com.android.tools.build.bundletool.model.targeting; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.util.stream.Collectors.joining; import com.android.bundle.Targeting.AssetsDirectoryTargeting; import com.android.bundle.Targeting.CountrySetTargeting; @@ -29,12 +30,13 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.Iterables; import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.Int32Value; import java.util.Collection; +import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -47,8 +49,8 @@ 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 String SEGMENT_SPLIT_CHARACTER = "#"; 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); @@ -56,12 +58,19 @@ public abstract class TargetedDirectorySegment { private static final String TCF_KEY = "tcf"; private static final String DEVICE_TIER_KEY = "tier"; + private static final ImmutableSet ALLOWED_NESTING_DIMENSIONS = + ImmutableSet.of( + TargetingDimension.COUNTRY_SET, + TargetingDimension.DEVICE_TIER, + TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + private static final int MAXIMUM_NESTING_DEPTH_ALLOWED = 2; + private static final ImmutableMap KEY_TO_DIMENSION = ImmutableMap.builder() + .put(COUNTRY_SET_KEY, TargetingDimension.COUNTRY_SET) + .put(DEVICE_TIER_KEY, TargetingDimension.DEVICE_TIER) .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(); @@ -71,17 +80,11 @@ public abstract class TargetedDirectorySegment { /** Positive targeting resolved from this directory name. */ public abstract AssetsDirectoryTargeting getTargeting(); - /** Get the targeting applied on this segment (if any). */ - public Optional getTargetingDimension() { - ImmutableList dimensions = - TargetingUtils.getTargetingDimensions(getTargeting()); - checkState(dimensions.size() <= 1); + public abstract ImmutableList getTargetingDimensionOrder(); - if (dimensions.isEmpty()) { - return Optional.empty(); - } else { - return Optional.of(dimensions.get(0)); - } + /** Get the targeting applied on this segment. */ + ImmutableList getTargetingDimensions() { + return TargetingUtils.getTargetingDimensions(getTargeting()); } /** Remove the targeting done for a specific dimension. */ @@ -104,40 +107,62 @@ && getTargeting().hasTextureCompressionFormat()) { return this; } - return new AutoValue_TargetedDirectorySegment(getName(), newTargeting.build()); + ImmutableList updatedTargetingDimensionOrder = + getTargetingDimensionOrder().stream() + .filter(targetingDimension -> !targetingDimension.equals(dimension)) + .collect(toImmutableList()); + return new AutoValue_TargetedDirectorySegment( + getName(), newTargeting.build(), updatedTargetingDimensionOrder); } public static TargetedDirectorySegment parse(String directorySegment) { - if (!directorySegment.contains("#")) { + if (!directorySegment.contains(SEGMENT_SPLIT_CHARACTER)) { return TargetedDirectorySegment.create(directorySegment); } - Matcher matcher = DIRECTORY_SEGMENT_PATTERN.matcher(directorySegment); - if (matcher.matches()) { - return TargetedDirectorySegment.create( - matcher.group("base"), matcher.group("key"), matcher.group("value")); + if (directorySegment.startsWith(SEGMENT_SPLIT_CHARACTER) + || directorySegment.endsWith(SEGMENT_SPLIT_CHARACTER)) { + throw InvalidBundleException.builder() + .withUserMessage( + "Cannot tokenize targeted directory '%s'. " + + "Expecting either '' or '#_' format.", + directorySegment) + .build(); } - throw InvalidBundleException.builder() - .withUserMessage( - "Cannot tokenize targeted directory '%s'. " - + "Expecting either '' or '#_' format.", - directorySegment) - .build(); + ImmutableList pathFragments = + ImmutableList.copyOf(directorySegment.split(SEGMENT_SPLIT_CHARACTER)); + String baseName = pathFragments.get(0); + ImmutableMap targetingKeyValues = + pathFragments.stream() + .skip(1) // first is base name. + .map(DimensionKeyValue::parse) + .collect( + toImmutableMap( + DimensionKeyValue::getDimensionKey, + DimensionKeyValue::getDimensionValue, + (v1, v2) -> { + throw InvalidBundleException.builder() + .withUserMessage( + "No directory should be targeted more than once on the same" + + " dimension. Found directory '%s' targeted multiple times on" + + " same dimension.", + directorySegment) + .build(); + })); + validateNestedTargetingDimensions(targetingKeyValues, directorySegment); + return TargetedDirectorySegment.create(baseName, targetingKeyValues); } /** Do the reverse of parse, returns the path represented by the segment. */ public String toPathSegment() { - ImmutableList dimensions = - TargetingUtils.getTargetingDimensions(getTargeting()); - checkState(dimensions.size() <= 1); - - Optional key = getTargetingKey(getTargeting()); - Optional value = getTargetingValue(getTargeting()); - - if (!key.isPresent() || !value.isPresent()) { - return getName(); - } - - return String.format("%s#%s_%s", getName(), key.get(), value.get()); + ImmutableList.Builder pathFragmentsBuilder = ImmutableList.builder(); + pathFragmentsBuilder.add(getName()); + pathFragmentsBuilder.addAll( + getTargetingDimensionOrder().stream() + .map(this::convertTargetingToPathSegment) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toImmutableList())); + return String.join("", pathFragmentsBuilder.build()); } /** @@ -150,58 +175,113 @@ public static boolean pathMayContain(String path, TargetingDimension dimension) return keys.stream().anyMatch(key -> path.contains("#" + key + "_")); } - private static TargetedDirectorySegment create(String name) { - return new AutoValue_TargetedDirectorySegment( - name, AssetsDirectoryTargeting.getDefaultInstance()); + /** Constructs targeting of directory in the given order of targeting dimension. */ + public static String constructTargetingSegmentPath( + AssetsDirectoryTargeting targeting, ImmutableList targetingOrder) { + return targetingOrder.stream() + .filter(dimension -> getTargetingValue(targeting, dimension).isPresent()) + .map( + dimension -> + String.format( + "#%s_%s", + getTargetingKey(dimension).get(), + getTargetingValue(targeting, dimension).get())) + .collect(joining("")); } - private static TargetedDirectorySegment create(String name, String key, String value) { - return new AutoValue_TargetedDirectorySegment( - name, toAssetsDirectoryTargeting(name, key, value)); + private Optional convertTargetingToPathSegment(TargetingDimension dimension) { + Optional key = getTargetingKey(dimension); + Optional value = getTargetingValue(getTargeting(), dimension); + if (key.isPresent() && value.isPresent()) { + return Optional.of(String.format("#%s_%s", key.get(), value.get())); + } + return Optional.empty(); } - /** Do the reverse of toAssetsDirectoryTargeting, return the key of the targeting. */ - private static Optional getTargetingKey(AssetsDirectoryTargeting targeting) { - ImmutableList dimensions = TargetingUtils.getTargetingDimensions(targeting); - checkArgument( - dimensions.size() <= 1, "Multiple targeting for a same directory is not supported"); - - if (targeting.hasLanguage()) { - return Optional.of(LANG_KEY); - } else if (targeting.hasTextureCompressionFormat()) { - 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); + private static Optional getTargetingValue( + AssetsDirectoryTargeting targeting, TargetingDimension dimension) { + switch (dimension) { + case COUNTRY_SET: + return targeting.getCountrySet().getValueList().stream().findFirst(); + case DEVICE_TIER: + return targeting.getDeviceTier().getValueList().stream() + .map(tier -> Integer.toString(tier.getValue())) + .findFirst(); + case LANGUAGE: + return targeting.getLanguage().getValueList().stream().findFirst(); + case TEXTURE_COMPRESSION_FORMAT: + return targeting.getTextureCompressionFormat().getValueList().stream() + .map( + tcfAlias -> + TextureCompressionUtils.TARGETING_TO_TEXTURE.getOrDefault( + tcfAlias.getAlias(), null)) + .filter(Objects::nonNull) + .findFirst(); + default: + return Optional.empty(); } + } - return Optional.empty(); + private static void validateNestedTargetingDimensions( + ImmutableMap keyValues, String directorySegment) { + if (keyValues.size() == 1) { + return; + } + if (keyValues.size() > MAXIMUM_NESTING_DEPTH_ALLOWED) { + throw InvalidBundleException.builder() + .withUserMessage( + "No directory should target more than two dimension. Found" + + " directory '%s' targeting more than two dimension.", + directorySegment) + .build(); + } + keyValues + .keySet() + .forEach( + key -> { + if (!KEY_TO_DIMENSION.containsKey(key)) { + throw InvalidBundleException.builder() + .withUserMessage( + "Unrecognized key: '%s' used in targeting of directory '%s'.", + key, directorySegment) + .build(); + } + if (!ALLOWED_NESTING_DIMENSIONS.contains(KEY_TO_DIMENSION.get(key))) { + throw InvalidBundleException.builder() + .withUserMessage( + "Targeting dimension '%s' should not be nested with other dimensions. Found" + + " directory '%s' which nests the dimension with other dimensions.", + KEY_TO_DIMENSION.get(key), directorySegment) + .build(); + } + }); } - /** Do the reverse of toAssetsDirectoryTargeting, return the value of the targeting. */ - private static Optional getTargetingValue(AssetsDirectoryTargeting targeting) { - ImmutableList dimensions = TargetingUtils.getTargetingDimensions(targeting); - checkArgument( - dimensions.size() <= 1, "Multiple targeting for a same directory is not supported"); + private static TargetedDirectorySegment create(String name) { + return new AutoValue_TargetedDirectorySegment( + name, AssetsDirectoryTargeting.getDefaultInstance(), ImmutableList.of()); + } - if (targeting.hasLanguage()) { - return Optional.of(Iterables.getOnlyElement(targeting.getLanguage().getValueList())); - } else if (targeting.hasTextureCompressionFormat()) { - return Optional.ofNullable( - TextureCompressionUtils.TARGETING_TO_TEXTURE.getOrDefault( - Iterables.getOnlyElement(targeting.getTextureCompressionFormat().getValueList()) - .getAlias(), - null)); - } else if (targeting.hasDeviceTier()) { - return Optional.of( - Integer.toString( - Iterables.getOnlyElement(targeting.getDeviceTier().getValueList()).getValue())); - } else if (targeting.hasCountrySet()) { - return Optional.of(Iterables.getOnlyElement(targeting.getCountrySet().getValueList())); - } + private static TargetedDirectorySegment create( + String name, ImmutableMap dimensionKeyValues) { + AssetsDirectoryTargeting directoryTargeting = + dimensionKeyValues.entrySet().stream() + .map( + keyValue -> + toAssetsDirectoryTargeting(name, keyValue.getKey(), keyValue.getValue())) + .reduce( + AssetsDirectoryTargeting.newBuilder(), + AssetsDirectoryTargeting.Builder::mergeFrom, + (builderA, builderB) -> builderA.mergeFrom(builderB.build())) + .build(); + ImmutableList targetingDimensionOrder = + dimensionKeyValues.keySet().stream().map(KEY_TO_DIMENSION::get).collect(toImmutableList()); + return new AutoValue_TargetedDirectorySegment( + name, directoryTargeting, targetingDimensionOrder); + } - return Optional.empty(); + private static Optional getTargetingKey(TargetingDimension dimension) { + return DIMENSION_TO_KEY.get(dimension).stream().findFirst(); } /** Returns the targeting specified by the directory name, alternatives are not generated. */ @@ -214,14 +294,14 @@ private static AssetsDirectoryTargeting toAssetsDirectoryTargeting( } switch (KEY_TO_DIMENSION.get(key)) { + case COUNTRY_SET: + return parseCountrySet(name, value); + case DEVICE_TIER: + return parseDeviceTier(name, value); case LANGUAGE: return parseLanguage(name, value); case TEXTURE_COMPRESSION_FORMAT: 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) diff --git a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java index a517e6c9..048fb22f 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/model/targeting/TargetingGenerator.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.model.targeting; import static com.android.tools.build.bundletool.model.BundleModule.ABI_SPLITTER; +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getAlternativeTargeting; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -39,7 +40,6 @@ import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; -import com.android.tools.build.bundletool.model.utils.TargetingProtoUtils; import com.android.tools.build.bundletool.model.utils.files.FileUtils; import com.google.common.base.Ascii; import com.google.common.collect.HashMultimap; @@ -47,12 +47,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import java.util.Collection; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; /** * From a list of raw directory names produces targeting. @@ -88,7 +86,6 @@ public Assets generateTargetingForAssets(Collection assetDirectories) { targetingByBaseName.put( targetedDirectory.getPathBaseName(), targetedDirectory.getLastSegment().getTargeting()); } - validateDimensions(targetingByBaseName); // Pass 2: Building the directory targeting proto using the targetingByBaseName map. @@ -117,17 +114,10 @@ public Assets generateTargetingForAssets(Collection assetDirectories) { continue; } targeting.mergeFrom( - // Remove oneself from the alternatives and merge them together. - Sets.difference( - targetingByBaseName.get(targetedDirectory.getSubPathBaseName(i)), - ImmutableSet.of(segment.getTargeting())) - .stream() - .map(TargetingProtoUtils::toAlternativeTargeting) - .reduce( - AssetsDirectoryTargeting.newBuilder(), - (builder, targetingValue) -> builder.mergeFrom(targetingValue), - (builderA, builderB) -> builderA.mergeFrom(builderB.build())) - .build()); + getAlternativeTargeting( + segment.getTargeting(), + ImmutableList.copyOf( + targetingByBaseName.get(targetedDirectory.getSubPathBaseName(i))))); } assetsBuilder.addDirectory( TargetedAssetsDirectory.newBuilder() @@ -138,36 +128,6 @@ public Assets generateTargetingForAssets(Collection assetDirectories) { return assetsBuilder.build(); } - /** Finds targeting dimension mismatches amongst multi-map entries. */ - private void validateDimensions(Multimap targetingMultimap) { - for (String baseName : targetingMultimap.keySet()) { - ImmutableList distinctDimensions = - targetingMultimap - .get(baseName) - .stream() - .map(TargetingUtils::getTargetingDimensions) - .flatMap(Collection::stream) - .distinct() - .collect(toImmutableList()); - if (distinctDimensions.size() > 1) { - throw InvalidBundleException.builder() - .withUserMessage( - "Expected at most one dimension type used for targeting of '%s'. " - + "However, the following dimensions were used: %s.", - baseName, joinDimensions(distinctDimensions)) - .build(); - } - } - } - - private static String joinDimensions(ImmutableList dimensions) { - return dimensions - .stream() - .map(dimension -> String.format("'%s'", dimension)) - .sorted() - .collect(Collectors.joining(", ")); - } - /** * Processes given native library directories, generating targeting based on their names. * 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 d3ae1d33..aed2d94d 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 @@ -16,11 +16,14 @@ package com.android.tools.build.bundletool.model.targeting; +import static com.android.tools.build.bundletool.model.utils.TargetingNormalizer.normalizeAssetsDirectoryTargeting; import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkVersionTargeting; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.MoreCollectors.toOptional; +import static java.util.function.Function.identity; import com.android.bundle.Files.ApexImages; import com.android.bundle.Files.Assets; @@ -39,8 +42,12 @@ import com.android.tools.build.bundletool.model.utils.TargetingProtoUtils; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimaps; import com.google.common.collect.Range; import com.google.common.collect.Sets; import com.google.common.collect.Streams; @@ -72,6 +79,39 @@ public static ImmutableList getTargetingDimensions( return dimensions.build(); } + /** Extracts specified dimension targeting from AssetsDirectoryTargeting. */ + public static Optional extractDimensionTargeting( + AssetsDirectoryTargeting directoryTargeting, TargetingDimension dimension) { + if (dimension.equals(TargetingDimension.ABI) && directoryTargeting.hasAbi()) { + return Optional.of( + AssetsDirectoryTargeting.newBuilder().setAbi(directoryTargeting.getAbi()).build()); + } else if (dimension.equals(TargetingDimension.COUNTRY_SET) + && directoryTargeting.hasCountrySet()) { + return Optional.of( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(directoryTargeting.getCountrySet()) + .build()); + } else if (dimension.equals(TargetingDimension.DEVICE_TIER) + && directoryTargeting.hasDeviceTier()) { + return Optional.of( + AssetsDirectoryTargeting.newBuilder() + .setDeviceTier(directoryTargeting.getDeviceTier()) + .build()); + } else if (dimension.equals(TargetingDimension.LANGUAGE) && directoryTargeting.hasLanguage()) { + return Optional.of( + AssetsDirectoryTargeting.newBuilder() + .setLanguage(directoryTargeting.getLanguage()) + .build()); + } else if (dimension.equals(TargetingDimension.TEXTURE_COMPRESSION_FORMAT) + && directoryTargeting.hasTextureCompressionFormat()) { + return Optional.of( + AssetsDirectoryTargeting.newBuilder() + .setTextureCompressionFormat(directoryTargeting.getTextureCompressionFormat()) + .build()); + } + return Optional.empty(); + } + /** * Given a set of potentially overlapping variant targetings generate smallest set of disjoint * variant targetings covering all of them. @@ -294,15 +334,7 @@ public static ImmutableSet extractCountrySets( } public static Optional generateAssetsTargeting(BundleModule module) { - ImmutableList assetDirectories = - module - .findEntriesUnderPath(BundleModule.ASSETS_DIRECTORY) - .map(ModuleEntry::getPath) - .filter(path -> path.getNameCount() > 1) - .map(ZipPath::getParent) - .distinct() - .collect(toImmutableList()); - + ImmutableList assetDirectories = getAssetDirectories(module); if (assetDirectories.isEmpty()) { return Optional.empty(); } @@ -355,6 +387,70 @@ public static Optional generateApexImagesTargeting(BundleModule modu new TargetingGenerator().generateTargetingForApexImages(apexImageFiles, hasBuildInfo)); } + /** Returns assets directories from a module. */ + public static ImmutableList getAssetDirectories(BundleModule module) { + return module + .findEntriesUnderPath(BundleModule.ASSETS_DIRECTORY) + .map(ModuleEntry::getPath) + .filter(path -> path.getNameCount() > 1) + .map(ZipPath::getParent) + .distinct() + .collect(toImmutableList()); + } + + public static AssetsDirectoryTargeting getAlternativeTargeting( + AssetsDirectoryTargeting targeting, ImmutableList allTargeting) { + ImmutableMap targetingByDimension = + splitAssetsDirectoryTargetingByDimensions(targeting); + return normalizeAssetsDirectoryTargeting( + getAssetsDirectoryTargetingByDimension(allTargeting).asMap().entrySet().stream() + .flatMap( + dimensionValuePair -> + dimensionValuePair.getValue().stream() + .filter( + value -> + !value.equals( + targetingByDimension.getOrDefault( + dimensionValuePair.getKey(), + AssetsDirectoryTargeting.getDefaultInstance()))) + .map(TargetingProtoUtils::toAlternativeTargeting)) + .reduce( + AssetsDirectoryTargeting.newBuilder(), + AssetsDirectoryTargeting.Builder::mergeFrom, + (builderA, builderB) -> builderA.mergeFrom(builderB.build())) + .build()); + } + + public static ImmutableMultimap + getAssetsDirectoryTargetingByDimension( + ImmutableList directoryAllTargeting) { + LinkedHashMultimap + assetsDirectoryTargetingByDimension = LinkedHashMultimap.create(); + directoryAllTargeting.stream() + .map(TargetingUtils::splitAssetsDirectoryTargetingByDimensions) + .map(Multimaps::forMap) + .forEach(assetsDirectoryTargetingByDimension::putAll); + if (assetsDirectoryTargetingByDimension.containsKey( + TargetingDimension.TEXTURE_COMPRESSION_FORMAT)) { + assetsDirectoryTargetingByDimension.put( + TargetingDimension.TEXTURE_COMPRESSION_FORMAT, + AssetsDirectoryTargeting.getDefaultInstance()); + } + if (assetsDirectoryTargetingByDimension.containsKey(TargetingDimension.COUNTRY_SET)) { + assetsDirectoryTargetingByDimension.put( + TargetingDimension.COUNTRY_SET, AssetsDirectoryTargeting.getDefaultInstance()); + } + return ImmutableMultimap.copyOf(assetsDirectoryTargetingByDimension); + } + + private static ImmutableMap + splitAssetsDirectoryTargetingByDimensions(AssetsDirectoryTargeting targeting) { + return getTargetingDimensions(targeting).stream() + .collect( + toImmutableMap( + identity(), dimension -> extractDimensionTargeting(targeting, dimension).get())); + } + private static Optional extractCountrySet(TargetedDirectory targetedDirectory) { return targetedDirectory .getTargeting(TargetingDimension.COUNTRY_SET) diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java b/src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java index c4a26a86..ea1cd436 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/LocaleConfigXmlInjector.java @@ -71,6 +71,7 @@ public ImmutableList process( // No need to inject a locales_config.xml if resources with all languages are // available inside the one APK. case STANDALONE: + case STANDALONE_FEATURE_MODULE: case INSTANT: case ARCHIVE: return splits; diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java b/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java index 90093a01..b4a3ec28 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/SplitsXmlInjector.java @@ -60,6 +60,10 @@ public ImmutableList process(VariantKey variantKey, Collection split.isBaseModuleSplit() ? processStandaloneVariant(split) : split) + .collect(toImmutableList()); case INSTANT: case ARCHIVE: return ImmutableList.copyOf(splits); 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 37b5f4ea..5f9edf48 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 @@ -24,6 +24,7 @@ import com.android.bundle.Targeting.Abi; import com.android.bundle.Targeting.AbiTargeting; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.AssetsDirectoryTargeting; import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; @@ -125,6 +126,28 @@ public static VariantTargeting normalizeVariantTargeting(VariantTargeting target return normalized.build(); } + public static AssetsDirectoryTargeting normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting targeting) { + AssetsDirectoryTargeting.Builder normalized = targeting.toBuilder(); + if (targeting.hasAbi()) { + normalized.setAbi(normalizeAbiTargeting(targeting.getAbi())); + } + if (targeting.hasCountrySet()) { + normalized.setCountrySet(normalizeCountrySetTargeting(targeting.getCountrySet())); + } + if (targeting.hasDeviceTier()) { + normalized.setDeviceTier(normalizeDeviceTierTargeting(targeting.getDeviceTier())); + } + if (targeting.hasLanguage()) { + normalized.setLanguage(normalizeLanguageTargeting(targeting.getLanguage())); + } + if (targeting.hasTextureCompressionFormat()) { + normalized.setTextureCompressionFormat( + normalizeTextureCompressionFormatTargeting(targeting.getTextureCompressionFormat())); + } + return normalized.build(); + } + private static AbiTargeting normalizeAbiTargeting(AbiTargeting targeting) { return AbiTargeting.newBuilder() .addAllValue(ImmutableList.sortedCopyOf(ABI_COMPARATOR, targeting.getValueList())) 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 1db56649..223cffc6 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.14.0"; + private static final String CURRENT_VERSION = "1.14.1"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java index 4ff98f9a..043d0ac3 100644 --- a/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/shards/StandaloneApksGenerator.java @@ -18,10 +18,18 @@ import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.standaloneApkVariantTargeting; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import com.android.bundle.Targeting; +import com.android.bundle.Targeting.ApkTargeting; +import com.android.tools.build.bundletool.mergers.AndroidManifestMerger; import com.android.tools.build.bundletool.mergers.ModuleSplitsToShardMerger; +import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleEntry; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; @@ -32,11 +40,14 @@ import com.android.tools.build.bundletool.splitters.CodeTransparencyInjector; import com.android.tools.build.bundletool.splitters.RuntimeEnabledSdkTableInjector; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import java.nio.file.Path; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import javax.inject.Inject; /** Generates standalone APKs sharded by required dimensions. */ @@ -46,6 +57,7 @@ public class StandaloneApksGenerator { private final ModuleSplitterForShards moduleSplitter; private final Sharder sharder; private final ModuleSplitsToShardMerger shardsMerger; + private final AppBundle appBundle; private final CodeTransparencyInjector codeTransparencyInjector; private final BinaryArtProfilesInjector binaryArtProfilesInjector; private final RuntimeEnabledSdkTableInjector runtimeEnabledSdkTableInjector; @@ -61,6 +73,7 @@ public StandaloneApksGenerator( this.moduleSplitter = moduleSplitter; this.sharder = sharder; this.shardsMerger = shardsMerger; + this.appBundle = appBundle; this.codeTransparencyInjector = new CodeTransparencyInjector(appBundle); this.binaryArtProfilesInjector = new BinaryArtProfilesInjector(appBundle); this.runtimeEnabledSdkTableInjector = new RuntimeEnabledSdkTableInjector(appBundle); @@ -100,17 +113,125 @@ public ImmutableList generateStandaloneApks( .stream()) .collect(toImmutableList()); + switch (appBundle + .getBundleConfig() + .getOptimizations() + .getStandaloneConfig() + .getFeatureModulesMode()) { + case SEPARATE_FEATURE_MODULES: + return generateStandaloneApkWithStandaloneFeatureModules(splits); + default: + return generateStandaloneApkWithFusedModule(splits); + } + } + + private ImmutableList generateStandaloneApkWithFusedModule( + ImmutableList splits) { Map, ImmutableList> dexCache = Maps.newHashMap(); return sharder.groupSplitsToShards(splits).stream() .map(unfusedShard -> shardsMerger.mergeSingleShard(unfusedShard, dexCache)) .map(StandaloneApksGenerator::setVariantTargetingAndSplitType) - .map(this::writeSourceStampInManifest) - .map(codeTransparencyInjector::inject) - .map(binaryArtProfilesInjector::inject) - .map(runtimeEnabledSdkTableInjector::inject) + .map(this::injectAdditionalEntriesIntoStandaloneApk) + .collect(toImmutableList()); + } + + private ImmutableList generateStandaloneApkWithStandaloneFeatureModules( + ImmutableList splits) { + ImmutableListMultimap splitsByModuleName = + splits.stream() + .collect(toImmutableListMultimap(ModuleSplit::getModuleName, Function.identity())); + + ImmutableSet uniqueApkTargetingFromAllModules = + splits.stream().map(ModuleSplit::getApkTargeting).collect(toImmutableSet()); + + return splitsByModuleName.keySet().stream() + .flatMap( + featureModuleName -> + generateStandaloneApkForFeatureModule( + appBundle.getModule(featureModuleName), + splitsByModuleName.get(featureModuleName), + uniqueApkTargetingFromAllModules) + .stream()) .collect(toImmutableList()); } + private ImmutableList generateStandaloneApkForFeatureModule( + BundleModule featureModule, + ImmutableList featureModuleSplit, + ImmutableSet uniqueApkTargeting) { + // First we take all splits which are available in this module by their targeting. + ImmutableMap featureSplitByTargeting = + featureModuleSplit.stream() + .collect(toImmutableMap(ModuleSplit::getApkTargeting, Function.identity())); + // Next we enrich empty module splits (without content) with targeting that is not used by this + // feature. + // + // If feature module doesn't contain native libraries but base module does (or vice-versa) we + // need to emulate + // ABI splits for feature module in order for {@link Sharder#groupSplitsToShards} to produce + // feature module + // APKs targeted by ABI dimension. + // + // For example: AAB with the following structure + // base + // |-- lib + // | |-- armeabi-v7a + // | |-- arm64-v8a + // feature + // should produce: base-armeabi_v7a.apk, base-arm64_v8a.apk, feature_armeabi_v7a.apk, + // feature-arm64_v8a.apk. + // As they later will be grouped into two variants: + // variant=1 (targeting ABI=armeabi-v7a): base-armeabi_v7a.apk, feature_armeabi_v7a.apk + // variant=2 (targeting ABI=arm64-v8a): base-arm64_v8a.apk, feature-arm64_v8a.apk + ImmutableList enrichedFeatureSplits = + uniqueApkTargeting.stream() + .map( + apkTargeting -> + featureSplitByTargeting.getOrDefault( + apkTargeting, createEmptyConfigSplit(featureModule, apkTargeting))) + .collect(toImmutableList()); + + return sharder.groupSplitsToShards(enrichedFeatureSplits).stream() + .map( + unfusedShard -> + shardsMerger.mergeSingleShard( + unfusedShard, + /* mergedDexCache= */ Maps.newHashMap(), + SplitType.STANDALONE_FEATURE_MODULE, + AndroidManifestMerger.manifestOverride(featureModule.getAndroidManifest()))) + .map( + shard -> + setVariantTargetingAndSplitTypeForStandaloneFeatureModule( + featureModule.getName(), shard)) + .map( + shard -> + featureModule.isBaseModule() + ? injectAdditionalEntriesIntoStandaloneApk(shard) + : shard) + .collect(toImmutableList()); + } + + private ModuleSplit createEmptyConfigSplit( + BundleModule featureModule, ApkTargeting apkTargeting) { + return ModuleSplit.builder() + .setAndroidManifest( + AndroidManifest.create( + AndroidManifest.createMinimalManifestTag(), appBundle.getVersion())) + .setModuleName(featureModule.getName()) + .setApkTargeting(apkTargeting) + .setVariantTargeting(Targeting.VariantTargeting.getDefaultInstance()) + .setMasterSplit(false) + .build(); + } + + private ModuleSplit injectAdditionalEntriesIntoStandaloneApk(ModuleSplit moduleSplit) { + ModuleSplit result = writeSourceStampInManifest(moduleSplit); + result = codeTransparencyInjector.inject(result); + result = binaryArtProfilesInjector.inject(result); + result = runtimeEnabledSdkTableInjector.inject(result); + return result; + } + /** Sets the variant targeting and split type to standalone. */ public static ModuleSplit setVariantTargetingAndSplitType(ModuleSplit shard) { return shard.toBuilder() @@ -119,6 +240,15 @@ public static ModuleSplit setVariantTargetingAndSplitType(ModuleSplit shard) { .build(); } + public static ModuleSplit setVariantTargetingAndSplitTypeForStandaloneFeatureModule( + BundleModuleName moduleName, ModuleSplit shard) { + return shard.toBuilder() + .setModuleName(moduleName) + .setVariantTargeting(standaloneApkVariantTargeting(shard)) + .setSplitType(SplitType.STANDALONE_FEATURE_MODULE) + .build(); + } + private ModuleSplit writeSourceStampInManifest(ModuleSplit shard) { return stampSource .map( diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjector.java b/src/main/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjector.java index 69fc9c4e..bd1a63e5 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjector.java @@ -18,10 +18,12 @@ import static com.android.tools.build.bundletool.sdkmodule.DexAndResourceRepackager.getCompatSdkConfigPathInAssets; import static java.nio.charset.StandardCharsets.UTF_8; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.ModuleEntry; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; +import com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.xml.XmlUtils; import com.google.common.annotations.VisibleForTesting; @@ -50,6 +52,7 @@ * * * com.sdk2 + * 11000 * RuntimeEnabledSdk-com.sdk2/CompatSdkConfig.xml * * @@ -64,6 +67,8 @@ public final class RuntimeEnabledSdkTableInjector { private static final String RUNTIME_ENABLED_SDK_TABLE_ELEMENT_NAME = "runtime-enabled-sdk-table"; private static final String RUNTIME_ENABELD_SDK_ELEMENT_NAME = "runtime-enabled-sdk"; private static final String SDK_PACKAGE_NAME_ELEMENT_NAME = "package-name"; + + private static final String SDK_VERSION_MAJOR_ELEMENT_NAME = "version-major"; private static final String COMPAT_CONFIG_PATH_ELEMENT_NAME = "compat-config-path"; private final AppBundle appBundle; @@ -85,7 +90,8 @@ public ModuleSplit inject(ModuleSplit split) { ByteSource.wrap( XmlUtils.documentToString( getRuntimeEnabledSdkTable( - appBundle.getRuntimeEnabledSdkDependencies().keySet())) + ImmutableSet.copyOf( + appBundle.getRuntimeEnabledSdkDependencies().values()))) .getBytes(UTF_8))) .build()) .build(); @@ -97,7 +103,7 @@ private boolean shouldAddRuntimeEnabledSdkTable(ModuleSplit split) { || (split.isMasterSplit() && split.isBaseModuleSplit())); } - private Document getRuntimeEnabledSdkTable(ImmutableSet sdkPackageNames) { + private Document getRuntimeEnabledSdkTable(ImmutableSet runtimeEnabledSdks) { Document runtimeEnabledSdkTable; try { runtimeEnabledSdkTable = @@ -106,28 +112,36 @@ private Document getRuntimeEnabledSdkTable(ImmutableSet sdkPackageNames) throw new IllegalStateException(e); } runtimeEnabledSdkTable.appendChild( - createRuntimeEnabledSdkTableNode(runtimeEnabledSdkTable, sdkPackageNames)); + createRuntimeEnabledSdkTableNode(runtimeEnabledSdkTable, runtimeEnabledSdks)); return runtimeEnabledSdkTable; } private Node createRuntimeEnabledSdkTableNode( - Document xmlFactory, ImmutableSet sdkPackageNames) { + Document xmlFactory, ImmutableSet runtimeEnabledSdks) { Element runtimeEnabledSdkTableNode = xmlFactory.createElement(RUNTIME_ENABLED_SDK_TABLE_ELEMENT_NAME); - sdkPackageNames.forEach( - sdkPackageName -> + runtimeEnabledSdks.forEach( + runtimeEnabledSdk -> runtimeEnabledSdkTableNode.appendChild( - createRuntimeEnabledSdkNode(xmlFactory, sdkPackageName))); + createRuntimeEnabledSdkNode(xmlFactory, runtimeEnabledSdk))); return runtimeEnabledSdkTableNode; } - private Node createRuntimeEnabledSdkNode(Document xmlFactory, String sdkPackageName) { + private Node createRuntimeEnabledSdkNode( + Document xmlFactory, RuntimeEnabledSdk runtimeEnabledSdk) { Element runtimeEnabledSdkNode = xmlFactory.createElement(RUNTIME_ENABELD_SDK_ELEMENT_NAME); Element sdkPackageNameNode = xmlFactory.createElement(SDK_PACKAGE_NAME_ELEMENT_NAME); - sdkPackageNameNode.setTextContent(sdkPackageName); + sdkPackageNameNode.setTextContent(runtimeEnabledSdk.getPackageName()); + Element sdkVersionMajorNode = xmlFactory.createElement(SDK_VERSION_MAJOR_ELEMENT_NAME); + sdkVersionMajorNode.setTextContent( + String.valueOf( + RuntimeEnabledSdkVersionEncoder.encodeSdkMajorAndMinorVersion( + runtimeEnabledSdk.getVersionMajor(), runtimeEnabledSdk.getVersionMinor()))); Element compatConfigPathNode = xmlFactory.createElement(COMPAT_CONFIG_PATH_ELEMENT_NAME); - compatConfigPathNode.setTextContent(getCompatSdkConfigPathInAssets(sdkPackageName)); + compatConfigPathNode.setTextContent( + getCompatSdkConfigPathInAssets(runtimeEnabledSdk.getPackageName())); runtimeEnabledSdkNode.appendChild(sdkPackageNameNode); + runtimeEnabledSdkNode.appendChild(sdkVersionMajorNode); runtimeEnabledSdkNode.appendChild(compatConfigPathNode); return runtimeEnabledSdkNode; } 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 6a0e42a7..a36cc285 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 @@ -44,6 +44,7 @@ public class AppBundleValidator { new BundleConfigValidator(), // More specific file validations. new EntryClashValidator(), + new NestedTargetingValidator(), new AbiParityValidator(), new TextureCompressionFormatParityValidator(), new DeviceTierParityValidator(), @@ -61,7 +62,8 @@ public class AppBundleValidator { new ResourceTableValidator(), new AssetModuleFilesValidator(), new CodeTransparencyValidator(), - new RuntimeEnabledSdkConfigValidator()); + new RuntimeEnabledSdkConfigValidator(), + new StandaloneFeatureModulesValidator()); private final ImmutableList allBundleSubValidators; private final ImmutableList allBundleFileSubValidators; 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 84b3bc0c..af7fe5fd 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 @@ -52,6 +52,7 @@ public class BundleModulesValidator { new AndroidManifestValidator(), // More specific file validations. new EntryClashValidator(), + new NestedTargetingValidator(), new AbiParityValidator(), new TextureCompressionFormatParityValidator(), new DeviceTierParityValidator(), 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 0f84291d..5f2c05ff 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 @@ -28,6 +28,7 @@ import com.android.bundle.Targeting.TextureCompressionFormat; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; import com.android.bundle.Targeting.TextureCompressionFormatTargeting; +import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; @@ -35,6 +36,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; +import java.util.Optional; /** Misc bundle validation functions. */ public final class BundleValidationUtils { diff --git a/src/main/java/com/android/tools/build/bundletool/validation/NestedTargetingValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/NestedTargetingValidator.java new file mode 100644 index 00000000..fa393290 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/NestedTargetingValidator.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.validation; + +import static com.android.tools.build.bundletool.model.targeting.TargetedDirectorySegment.constructTargetingSegmentPath; +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getAssetDirectories; +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getAssetsDirectoryTargetingByDimension; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.Comparator.naturalOrder; + +import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.ZipPath; +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.android.tools.build.bundletool.model.targeting.TargetingUtils; +import com.android.tools.build.bundletool.model.utils.files.FileUtils; +import com.google.auto.value.AutoValue; +import com.google.common.collect.Comparators; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.LinkedHashMultimap; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** Validator class for nested targeting validations. */ +public class NestedTargetingValidator extends SubValidator { + + /** Validates nested targeting in all the modules. */ + @Override + public void validateAllModules(ImmutableList modules) { + modules.forEach(NestedTargetingValidator::validateNestedTargetingInModule); + if (modules.stream() + .map(NestedTargetingValidator::getDirectoryAllNestedTargetingByPathBaseName) + .filter( + moduleDirectoryAllNestedTargetingByPathBaseName -> + !moduleDirectoryAllNestedTargetingByPathBaseName.isEmpty()) + .map(ImmutableSetMultimap::asMap) + .map(ImmutableMap::values) + .distinct() + .count() + > 1) { + throw InvalidBundleException.builder() + .withUserMessage( + "Found different nested targeting across different modules. Please make sure all" + + " modules use same nested targeting.") + .build(); + } + } + + private static void validateNestedTargetingInModule(BundleModule module) { + ImmutableSetMultimap + directoryAllNestedTargetingByPathBaseName = + getDirectoryAllNestedTargetingByPathBaseName(module); + if (directoryAllNestedTargetingByPathBaseName.isEmpty()) { + return; + } + validateAllDirectoryNestedTargetingAreDistinct( + module, directoryAllNestedTargetingByPathBaseName); + validateAllDirectoriesHaveSameNestedTargeting( + module, directoryAllNestedTargetingByPathBaseName); + ImmutableSet supportedNestedTargeting = + ImmutableSet.copyOf( + directoryAllNestedTargetingByPathBaseName.asMap().values().stream().findAny().get()); + validateNestingDimensionOrder(module, supportedNestedTargeting); + validateNestedTargetingCoversAllCartesianProductPoints(module, supportedNestedTargeting); + } + + private static void validateNestedTargetingCoversAllCartesianProductPoints( + BundleModule module, ImmutableSet directoryAllNestedTargeting) { + // 1. Create dimension values map. + ImmutableMultimap + assetsDirectoryTargetingByDimension = + getAssetsDirectoryTargetingByDimension( + directoryAllNestedTargeting.stream() + .map(DirectoryNestedTargeting::getTargeting) + .collect(toImmutableList())); + + // 2. Create cartesian product of all the dimension values and populate in the universe. + ImmutableList nestedTargetingUniverse = + ImmutableList.of(AssetsDirectoryTargeting.getDefaultInstance()); + for (Collection values : + assetsDirectoryTargetingByDimension.asMap().values()) { + nestedTargetingUniverse = + nestedTargetingUniverse.stream() + .flatMap( + targeting -> + values.stream() + .map(targeting1 -> targeting.toBuilder().mergeFrom(targeting1).build())) + .collect(toImmutableList()); + } + + // 3. Check that the supportedNestedTargeting covers all points in the universe. + if (!directoryAllNestedTargeting.stream() + .map(DirectoryNestedTargeting::getTargeting) + .collect(toImmutableList()) + .containsAll(nestedTargetingUniverse)) { + throw InvalidBundleException.builder() + .withUserMessage( + "Module '%s' uses nested targeting but does not define targeting for all of the" + + " points in the cartesian product of dimension values used.", + module.getName()) + .build(); + } + } + + /** Validates that all the directories/folders have same targeting. */ + private static void validateAllDirectoriesHaveSameNestedTargeting( + BundleModule module, + ImmutableSetMultimap + directoryAllNestedTargetingByPathBaseName) { + if (directoryAllNestedTargetingByPathBaseName.asMap().values().stream().distinct().count() + > 1) { + throw InvalidBundleException.builder() + .withUserMessage( + "Found directories targeted on different set of targeting dimensions in module '%s'." + + " Please make sure all directories are targeted on same set of targeting" + + " dimensions in same order.", + module.getName()) + .build(); + } + } + + private static void validateNestingDimensionOrder( + BundleModule module, ImmutableSet directoryAllNestedTargeting) { + ImmutableList targetingDimensionOrderRef = + directoryAllNestedTargeting.stream() + .map(DirectoryNestedTargeting::getTargetingDimensionOrder) + .max(Comparator.comparing(List::size)) + .get(); + ImmutableSet targetingDimensionOrderRefSet = + ImmutableSet.copyOf(targetingDimensionOrderRef); + + directoryAllNestedTargeting.stream() + .map(DirectoryNestedTargeting::getTargetingDimensionOrder) + .forEach( + dimensionOrder -> { + if (!targetingDimensionOrderRefSet.containsAll(dimensionOrder)) { + throw InvalidBundleException.builder() + .withUserMessage( + "Found directory targeted on different set of targeting dimensions in" + + " module '%s'. Targeting Used: '%s'. Please make sure all directories" + + " are targeted on same set of targeting dimensions in same order.", + module.getName(), + directoryAllTargetingToString(directoryAllNestedTargeting)) + .build(); + } + if (!Comparators.isInOrder( + dimensionOrder.stream() + .map(targetingDimensionOrderRef::indexOf) + .collect(toImmutableList()), + naturalOrder())) { + throw InvalidBundleException.builder() + .withUserMessage( + "Found directory targeted on different order of targeting dimensions in" + + " module '%s'. Targeting Used: '%s'. Please make sure all directories" + + " are targeted on same set of targeting dimensions in same order.", + module.getName(), + directoryAllTargetingToString(directoryAllNestedTargeting)) + .build(); + } + }); + } + + /** + * Validates that the nested targeting applied on a directory are distinct i.e. one can not have + * the directories 'a/b/c#countries_latam#tcf_astc' and 'a/b/c#tcf_astc#countries_latam' + * simultaneously. + */ + private static void validateAllDirectoryNestedTargetingAreDistinct( + BundleModule module, + ImmutableSetMultimap + directoryAllNestedTargetingByPathBaseName) { + directoryAllNestedTargetingByPathBaseName + .asMap() + .forEach( + (key, value) -> { + if (value.stream().map(DirectoryNestedTargeting::getTargeting).distinct().count() + != value.size()) { + throw InvalidBundleException.builder() + .withUserMessage( + "Found multiple directories using same targeting values in module '%s'." + + " Directory '%s' is targeted on following dimension values some of" + + " which vary only in nesting order: '%s'.", + module.getName(), + key, + directoryAllTargetingToString(ImmutableSet.copyOf(value))) + .build(); + } + }); + } + + private static ImmutableSetMultimap + getDirectoryAllNestedTargetingByPathBaseName(BundleModule module) { + ImmutableList assetDirectories = getAssetDirectories(module); + // This map stores all targeting (single & nested) for a path base name. + LinkedHashMultimap directoryAllTargetingByPathBaseName = + LinkedHashMultimap.create(); + FileUtils.toPathWalkingOrder(assetDirectories) + .forEach( + assetDirectory -> { + TargetedDirectory targetedDirectory = TargetedDirectory.parse(assetDirectory); + directoryAllTargetingByPathBaseName.put( + targetedDirectory.getPathBaseName(), + DirectoryNestedTargeting.create( + targetedDirectory.getLastSegment().getTargeting(), + targetedDirectory.getLastSegment().getTargetingDimensionOrder())); + }); + + // This map stores all nested targeting for a path base name. + LinkedHashMultimap directoryAllNestedTargetingByPathBaseName = + LinkedHashMultimap.create(); + directoryAllTargetingByPathBaseName.asMap().entrySet().stream() + .filter( + directoryTargeting -> + targetedOnMultipleDimension(ImmutableList.copyOf(directoryTargeting.getValue()))) + .forEach( + directoryAllTargeting -> + directoryAllTargeting.getValue().stream() + .forEach( + directoryTargeting -> + directoryAllNestedTargetingByPathBaseName.put( + directoryAllTargeting.getKey(), directoryTargeting))); + return ImmutableSetMultimap.copyOf(directoryAllNestedTargetingByPathBaseName); + } + + private static boolean targetedOnMultipleDimension( + ImmutableList directoryNestedTargetingList) { + return directoryNestedTargetingList.stream() + .flatMap( + directoryNestedTargeting -> + TargetingUtils.getTargetingDimensions(directoryNestedTargeting.getTargeting()) + .stream()) + .distinct() + .count() + > 1; + } + + private static String directoryAllTargetingToString( + ImmutableSet directoryAllTargeting) { + return directoryAllTargeting.stream() + .map( + targeting -> + constructTargetingSegmentPath( + targeting.getTargeting(), targeting.getTargetingDimensionOrder())) + .collect(toImmutableList()) + .toString(); + } + + /** Represents the nested targeting attributed to a directory. */ + @AutoValue + abstract static class DirectoryNestedTargeting { + + public static NestedTargetingValidator.DirectoryNestedTargeting create( + AssetsDirectoryTargeting directoryTargeting, + ImmutableList targetingDimensionOrder) { + return new AutoValue_NestedTargetingValidator_DirectoryNestedTargeting( + directoryTargeting, targetingDimensionOrder); + } + + public abstract AssetsDirectoryTargeting getTargeting(); + + public abstract ImmutableList getTargetingDimensionOrder(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidator.java index fdc42aa5..b62c1ff9 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidator.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.validation; +import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.SDK_PATCH_VERSION_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MAJOR_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MINOR_MAX_VALUE; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -124,6 +125,12 @@ private static void validateRuntimeEnabledSdk(RuntimeEnabledSdk runtimeEnabledSd runtimeEnabledSdk.getBuildTimeVersionPatch() >= 0, "Found dependency on runtime-enabled SDK '%s' with a negative patch version.", runtimeEnabledSdk.getPackageName()); + validate( + runtimeEnabledSdk.getBuildTimeVersionPatch() <= SDK_PATCH_VERSION_MAX_VALUE, + "Found dependency on runtime-enabled SDK '%s' with illegal patch version. Patch" + + " version must be <= %d.", + runtimeEnabledSdk.getPackageName(), + SDK_PATCH_VERSION_MAX_VALUE); validate( FingerprintDigestValidator.isValidFingerprintDigest( runtimeEnabledSdk.getCertificateDigest()), diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java index 0f1eee84..05f4133f 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java @@ -18,10 +18,10 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.ACTIVITY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_LOCATION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.META_DATA_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_TREE_ELEMENT_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.PROPERTY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PROVIDER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.RECEIVER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_LIBRARY_ELEMENT_NAME; @@ -40,7 +40,7 @@ public class SdkAndroidManifestValidator extends SubValidator { public void validateModule(BundleModule module) { AndroidManifest manifest = module.getAndroidManifest(); validateNoSdkLibraryElement(manifest); - validateNoSdkPatchVersionProperty(manifest); + validateNoSdkPatchVersionMetadata(manifest); validateInternalOnlyIfInstallLocationSet(manifest); validateNoPermissions(manifest); validateNoSharedUserId(manifest); @@ -57,12 +57,12 @@ private void validateNoSdkLibraryElement(AndroidManifest manifest) { } } - private void validateNoSdkPatchVersionProperty(AndroidManifest manifest) { - if (manifest.getSdkPatchVersionProperty().isPresent()) { + private void validateNoSdkPatchVersionMetadata(AndroidManifest manifest) { + if (manifest.getSdkPatchVersionMetadata().isPresent()) { throw InvalidBundleException.builder() .withUserMessage( "<%s> cannot be declared with name='%s' in the manifest of an SDK bundle.", - PROPERTY_ELEMENT_NAME, SDK_PATCH_VERSION_ATTRIBUTE_NAME) + META_DATA_ELEMENT_NAME, SDK_PATCH_VERSION_ATTRIBUTE_NAME) .build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidator.java index fa3b60b1..c9f2f8cc 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidator.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.validation; +import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.SDK_PATCH_VERSION_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MAJOR_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MINOR_MAX_VALUE; import static com.android.tools.build.bundletool.model.utils.BundleParser.readSdkModulesConfig; @@ -66,9 +67,10 @@ private void validateSdkVersion(SdkModulesConfig sdkModulesConfig) { .build(); } - if (sdkVersion.getPatch() < 0) { + if (sdkVersion.getPatch() < 0 || sdkVersion.getPatch() > SDK_PATCH_VERSION_MAX_VALUE) { throw InvalidBundleException.builder() - .withUserMessage("SDK patch version must be a non-negative integer") + .withUserMessage( + "SDK patch version must be an integer between 0 and %d", SDK_PATCH_VERSION_MAX_VALUE) .build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/StandaloneFeatureModulesValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/StandaloneFeatureModulesValidator.java new file mode 100644 index 00000000..e36c7e6a --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/StandaloneFeatureModulesValidator.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.validation; + +import com.android.bundle.Config.StandaloneConfig.FeatureModulesMode; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.ModuleDeliveryType; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; + +/** + * Validates bundles with enabled SEPARATE_FEATURE_MODULES. + * + *

Only AABs with only on-demand feature modules and minSdk < 21 are allowed to have this flag. + */ +final class StandaloneFeatureModulesValidator extends SubValidator { + + @Override + public void validateBundle(AppBundle bundle) { + if (!bundle.hasBaseModule() + || !bundle + .getBundleConfig() + .getOptimizations() + .getStandaloneConfig() + .getFeatureModulesMode() + .equals(FeatureModulesMode.SEPARATE_FEATURE_MODULES)) { + return; + } + if (bundle.getBaseModule().getAndroidManifest().getEffectiveMinSdkVersion() >= 21) { + throw InvalidBundleException.createWithUserMessage( + "STANDALONE_FEATURE_MODULES can only be used for " + + "Android App Bundles with minSdk < 21."); + } + boolean onlyOnDemandFeatures = + bundle.getFeatureModules().values().stream() + .filter(module -> !module.isBaseModule()) + .allMatch(module -> module.getDeliveryType() == ModuleDeliveryType.NO_INITIAL_INSTALL); + if (!onlyOnDemandFeatures) { + throw InvalidBundleException.createWithUserMessage( + "Only on-demand feature modules are supported for " + + "Android App Bundles with STANDALONE_FEATURE_MODULES enabled."); + } + if (!bundle.getAssetModules().isEmpty()) { + throw InvalidBundleException.createWithUserMessage( + "Asset modules are not supported for " + + "Android App Bundles with STANDALONE_FEATURE_MODULES enabled."); + } + } +} diff --git a/src/main/proto/config.proto b/src/main/proto/config.proto index 64e60901..3b3f3ac7 100644 --- a/src/main/proto/config.proto +++ b/src/main/proto/config.proto @@ -239,6 +239,19 @@ message StandaloneConfig { // application developers. NEVER_MERGE = 1; } + + // Defines how to deal with feature modules in standalone variants (minSdk < + // 21). + FeatureModulesMode feature_modules_mode = 4; + + enum FeatureModulesMode { + // Default mode which fuses feature modules with respect to its + // fusing attribute into base.apk. + FUSED_FEATURE_MODULES = 0; + // Advanced mode, which allows to generate a single separate apk per each + // feature module in variants with minSdk < 21. + SEPARATE_FEATURE_MODULES = 1; + } } message SplitDimension { diff --git a/src/main/proto/sdk_bundle_config.proto b/src/main/proto/sdk_bundle_config.proto index de34392a..9984d970 100644 --- a/src/main/proto/sdk_bundle_config.proto +++ b/src/main/proto/sdk_bundle_config.proto @@ -13,10 +13,13 @@ message SdkBundleConfig { message SdkBundle { // Package name of the SDK bundle. string package_name = 1; + // Major version of the SDK bundle. int32 version_major = 2; + // Minor version of the SDK bundle. int32 version_minor = 3; + // Patch version of the SDK bundle. // The dependency on a specific patch version is a build-time soft dependency, // that ensures reproducibility of local builds; it does not imply that all @@ -25,7 +28,21 @@ message SdkBundle { // while serve the latest available patch for the given major.minor version of // the SDK. int32 build_time_version_patch = 4; + // SHA-256 hash of the SDK's signing certificate, represented as a string of // bytes in hexadecimal form, with ':' separating the bytes. string certificate_digest = 5; + + // Whether the dependency is optional or required. Only required dependencies + // will be included in the final POM file. + // Unspecified dependency types will be treated as required. + SdkDependencyType dependency_type = 6; +} + +enum SdkDependencyType { + SDK_DEPENDENCY_TYPE_UNSPECIFIED = 0; + // The dependency should be installed automatically. + SDK_DEPENDENCY_TYPE_REQUIRED = 1; + // The dependency is only needed at compile time. + SDK_DEPENDENCY_TYPE_OPTIONAL = 2; } 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 49969fd1..bac35b0e 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 @@ -107,6 +107,7 @@ import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.CertificateFactory; +import com.android.tools.build.bundletool.testing.FakeSigningConfigurationProvider; import com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider; import com.android.tools.build.bundletool.testing.SdkBundleBuilder; import com.android.tools.build.bundletool.testing.TargetingUtils; @@ -1913,7 +1914,8 @@ public void settingBothSigningConfigAndSigningConfigProvider_throwsError() { () -> BuildApksCommand.builder() .setSigningConfiguration(signingConfig) - .setSigningConfigurationProvider(apkDescription -> apksigSigningConfig) + .setSigningConfigurationProvider( + new FakeSigningConfigurationProvider(apkDescription -> apksigSigningConfig)) .build()); assertThat(e) diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java index e2d36466..b4fb946d 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksManagerTest.java @@ -166,8 +166,10 @@ import com.android.bundle.Config.ResourceOptimizations.ResourceTypeAndName; import com.android.bundle.Config.SplitDimension.Value; import com.android.bundle.Config.StandaloneConfig; +import com.android.bundle.Config.StandaloneConfig.FeatureModulesMode; import com.android.bundle.Config.UncompressDexFiles.UncompressedDexTargetSdk; import com.android.bundle.Files.ApexImages; +import com.android.bundle.Files.Assets; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.bundle.Targeting.Abi; @@ -215,12 +217,14 @@ import com.android.tools.build.bundletool.testing.truth.zip.TruthZip; import com.android.zipflinger.ZipMap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Maps; +import com.google.common.collect.Multimaps; import com.google.common.hash.Hashing; import com.google.common.io.ByteSource; import com.google.common.io.ByteStreams; @@ -230,6 +234,7 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Int32Value; +import com.google.protobuf.TextFormat; import dagger.Component; import java.io.File; import java.io.FileOutputStream; @@ -2398,6 +2403,201 @@ public void buildApksCommand_splitApks_localeConfigXmlGenerated() throws Excepti } } + @Test + public void buildApksCommand_splitApks_nestedTargeting_countryAndTexture() throws Exception { + Assets.Builder assetsConfigBuilder = Assets.newBuilder(); + TextFormat.merge( + TestData.openReader("testdata/nested_targeting/country_tcf_assets_config.textpb"), + assetsConfigBuilder); + AppBundle appBundle = + new AppBundleBuilder() + .addModule("base", builder -> builder.setManifest(androidManifest("com.test.app"))) + .addModule( + "assetpack1", + builder -> + builder + .addFile("assets/textures#countries_latam#tcf_astc/texture.dat") + .addFile("assets/textures#countries_latam#tcf_pvrtc/texture.dat") + .addFile("assets/textures#countries_latam/texture.dat") + .addFile("assets/textures#countries_sea#tcf_astc/texture.dat") + .addFile("assets/textures#countries_sea#tcf_pvrtc/texture.dat") + .addFile("assets/textures#countries_sea/texture.dat") + .addFile("assets/textures#tcf_astc/texture.dat") + .addFile("assets/textures#tcf_pvrtc/texture.dat") + .addFile("assets/textures/texture.dat") + .setAssetsConfig(assetsConfigBuilder.build()) + .setManifest( + androidManifestForAssetModule( + "com.test.app", withInstallTimeDelivery()))) + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension( + Value.COUNTRY_SET, + /* negate= */ false, + /* stripSuffix= */ true, + /* defaultSuffix= */ "latam") + .addSplitDimension( + Value.TEXTURE_COMPRESSION_FORMAT, + /* negate= */ false, + /* stripSuffix= */ true, + /* defaultSuffix= */ "astc") + .build()) + .build(); + + TestComponent.useTestModule( + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + buildApksManager.execute(); + + BuildApksResult.Builder expectedBuildApksResultBuilder = BuildApksResult.newBuilder(); + TextFormat.merge( + TestData.openReader("testdata/nested_targeting/country_tcf_toc.textpb"), + expectedBuildApksResultBuilder); + BuildApksResult expectedBuildApksResult = expectedBuildApksResultBuilder.build(); + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult generatedBuildApksResult = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(generatedBuildApksResult) + .comparingExpectedFieldsOnly() + .isEqualTo(expectedBuildApksResult); + expectedBuildApksResult.getAssetSliceSetList().stream() + .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream()) + .forEach(assetSlice -> assertThat(apkSetFile).hasFile(assetSlice.getPath())); + expectedBuildApksResult.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) + .forEach(splitApk -> assertThat(apkSetFile).hasFile(splitApk.getPath())); + } + + @Test + public void buildApksCommand_splitApks_nestedTargeting_tierAndTexture() throws Exception { + Assets.Builder assetsConfigBuilder = Assets.newBuilder(); + TextFormat.merge( + TestData.openReader("testdata/nested_targeting/tier_tcf_assets_config.textpb"), + assetsConfigBuilder); + AppBundle appBundle = + new AppBundleBuilder() + .addModule("base", builder -> builder.setManifest(androidManifest("com.test.app"))) + .addModule( + "assetpack1", + builder -> + builder + .addFile("assets/textures#tier_2#tcf_astc/texture.dat") + .addFile("assets/textures#tier_2#tcf_pvrtc/texture.dat") + .addFile("assets/textures#tier_2/texture.dat") + .addFile("assets/textures#tier_1#tcf_astc/texture.dat") + .addFile("assets/textures#tier_1#tcf_pvrtc/texture.dat") + .addFile("assets/textures#tier_1/texture.dat") + .addFile("assets/textures#tier_0#tcf_astc/texture.dat") + .addFile("assets/textures#tier_0#tcf_pvrtc/texture.dat") + .addFile("assets/textures#tier_0/texture.dat") + .setAssetsConfig(assetsConfigBuilder.build()) + .setManifest( + androidManifestForAssetModule( + "com.test.app", withInstallTimeDelivery()))) + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension( + Value.DEVICE_TIER, + /* negate= */ false, + /* stripSuffix= */ true, + /* defaultSuffix= */ "0") + .addSplitDimension( + Value.TEXTURE_COMPRESSION_FORMAT, + /* negate= */ false, + /* stripSuffix= */ true, + /* defaultSuffix= */ "astc") + .build()) + .build(); + + TestComponent.useTestModule( + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + buildApksManager.execute(); + + BuildApksResult.Builder expectedBuildApksResultBuilder = BuildApksResult.newBuilder(); + TextFormat.merge( + TestData.openReader("testdata/nested_targeting/tier_tcf_toc.textpb"), + expectedBuildApksResultBuilder); + BuildApksResult expectedBuildApksResult = expectedBuildApksResultBuilder.build(); + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult generatedBuildApksResult = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(generatedBuildApksResult) + .comparingExpectedFieldsOnly() + .isEqualTo(expectedBuildApksResult); + expectedBuildApksResult.getAssetSliceSetList().stream() + .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream()) + .forEach(assetSlice -> assertThat(apkSetFile).hasFile(assetSlice.getPath())); + expectedBuildApksResult.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) + .forEach(splitApk -> assertThat(apkSetFile).hasFile(splitApk.getPath())); + } + + @Test + public void buildApksCommand_splitApks_nestedTargeting_countryAndTier() throws Exception { + Assets.Builder assetsConfigBuilder = Assets.newBuilder(); + TextFormat.merge( + TestData.openReader("testdata/nested_targeting/country_tier_assets_config.textpb"), + assetsConfigBuilder); + AppBundle appBundle = + new AppBundleBuilder() + .addModule("base", builder -> builder.setManifest(androidManifest("com.test.app"))) + .addModule( + "assetpack1", + builder -> + builder + .addFile("assets/textures#countries_latam#tier_2/texture.dat") + .addFile("assets/textures#countries_latam#tier_1/texture.dat") + .addFile("assets/textures#countries_latam#tier_0/texture.dat") + .addFile("assets/textures#countries_sea#tier_2/texture.dat") + .addFile("assets/textures#countries_sea#tier_1/texture.dat") + .addFile("assets/textures#countries_sea#tier_0/texture.dat") + .addFile("assets/textures#tier_2/texture.dat") + .addFile("assets/textures#tier_1/texture.dat") + .addFile("assets/textures#tier_0/texture.dat") + .setAssetsConfig(assetsConfigBuilder.build()) + .setManifest( + androidManifestForAssetModule( + "com.test.app", withInstallTimeDelivery()))) + .setBundleConfig( + BundleConfigBuilder.create() + .addSplitDimension( + Value.COUNTRY_SET, + /* negate= */ false, + /* stripSuffix= */ true, + /* defaultSuffix= */ "latam") + .addSplitDimension( + Value.DEVICE_TIER, + /* negate= */ false, + /* stripSuffix= */ true, + /* defaultSuffix= */ "0") + .build()) + .build(); + + TestComponent.useTestModule( + this, + createTestModuleBuilder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + buildApksManager.execute(); + + BuildApksResult.Builder expectedBuildApksResultBuilder = BuildApksResult.newBuilder(); + TextFormat.merge( + TestData.openReader("testdata/nested_targeting/country_tier_toc.textpb"), + expectedBuildApksResultBuilder); + BuildApksResult expectedBuildApksResult = expectedBuildApksResultBuilder.build(); + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult generatedBuildApksResult = extractTocFromApkSetFile(apkSetFile, outputDir); + assertThat(generatedBuildApksResult) + .comparingExpectedFieldsOnly() + .isEqualTo(expectedBuildApksResult); + expectedBuildApksResult.getAssetSliceSetList().stream() + .flatMap(assetSliceSet -> assetSliceSet.getApkDescriptionList().stream()) + .forEach(assetSlice -> assertThat(apkSetFile).hasFile(assetSlice.getPath())); + expectedBuildApksResult.getVariantList().stream() + .flatMap(variant -> variant.getApkSetList().stream()) + .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) + .forEach(splitApk -> assertThat(apkSetFile).hasFile(splitApk.getPath())); + } + @Test public void buildApksCommand_standalone_oneModuleOneVariant() throws Exception { AppBundle appBundle = @@ -3434,6 +3634,96 @@ public void buildApksCommand_standalone_mixedTargeting() throws Exception { } } + @Test + public void buildApksCommand_standalone_standaloneFeatureModules_mixedTargeting() + throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.SEPARATE_FEATURE_MODULES) + .build()) + .addModule( + "base", + builder -> + builder + .setManifest(androidManifest("com.test.app")) + .setResourceTable(resourceTableWithTestLabel("Test feature"))) + .addModule( + "feature_abi_lib", + builder -> + builder + .addFile("assets/a.txt") + .addFile("lib/x86/libfeature.so") + .addFile("lib/x86_64/libfeature.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)), + targetedNativeDirectory( + "lib/x86_64", nativeDirectoryTargeting(X86_64)))) + .setManifest( + androidManifestForFeature( + "com.test.app", + withTitle("@string/test_label", TEST_LABEL_RESOURCE_ID), + // Disable fusing as this should not affect DFMs in + // SEPARATE_FEATURE_MODULES mode. + withFusingAttribute(false)))) + .build(); + TestComponent.useTestModule( + this, + createTestModuleBuilder() + .withAppBundle(appBundle) + .withOutputPath(outputFilePath) + .withOptimizationDimensions(ABI) + .build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + ImmutableListMultimap standaloneApksByAbi = + Multimaps.index( + apkDescriptions(standaloneApkVariants(result)), + apkDesc -> getOnlyElement(apkDesc.getTargeting().getAbiTargeting().getValueList())); + + assertThat(standaloneApksByAbi.keySet()).containsExactly(toAbi(X86), toAbi(X86_64)); + assertThat(standaloneApksByAbi.get(toAbi(X86)).stream().map(ApkDescription::getPath)) + .containsExactly("standalones/standalone-x86.apk", "standalones/feature_abi_lib-x86.apk"); + assertThat(standaloneApksByAbi.get(toAbi(X86_64)).stream().map(ApkDescription::getPath)) + .containsExactly( + "standalones/standalone-x86_64.apk", "standalones/feature_abi_lib-x86_64.apk"); + + File baseX86ApkFile = + extractFromApkSetFile(apkSetFile, "standalones/standalone-x86.apk", outputDir); + File baseX64ApkFile = + extractFromApkSetFile(apkSetFile, "standalones/standalone-x86_64.apk", outputDir); + File featureX86ApkFile = + extractFromApkSetFile(apkSetFile, "standalones/feature_abi_lib-x86.apk", outputDir); + File featureX64ApkFile = + extractFromApkSetFile(apkSetFile, "standalones/feature_abi_lib-x86_64.apk", outputDir); + try (ZipFile baseX86ApkZip = new ZipFile(baseX86ApkFile)) { + assertThat(baseX86ApkZip).doesNotHaveFile("lib/x86/libfeature.so"); + assertThat(baseX86ApkZip).doesNotHaveFile("lib/x86_64/libfeature.so"); + assertThat(baseX86ApkZip).doesNotHaveFile("assets/a.txt"); + } + try (ZipFile baseX64ApkZip = new ZipFile(baseX64ApkFile)) { + assertThat(baseX64ApkZip).doesNotHaveFile("lib/x86/libfeature.so"); + assertThat(baseX64ApkZip).doesNotHaveFile("lib/x86_64/libfeature.so"); + assertThat(baseX64ApkZip).doesNotHaveFile("assets/a.txt"); + } + try (ZipFile featureX86ApkZip = new ZipFile(featureX86ApkFile)) { + assertThat(featureX86ApkZip).hasFile("lib/x86/libfeature.so"); + assertThat(featureX86ApkZip).doesNotHaveFile("lib/x86_64/libfeature.so"); + assertThat(featureX86ApkZip).hasFile("assets/a.txt"); + } + try (ZipFile featureX64ApkZip = new ZipFile(featureX64ApkFile)) { + assertThat(featureX64ApkZip).doesNotHaveFile("lib/x86/libfeature.so"); + assertThat(featureX64ApkZip).hasFile("lib/x86_64/libfeature.so"); + assertThat(featureX64ApkZip).hasFile("assets/a.txt"); + } + } + @Test public void buildApksCommand_standalone_noTextureTargeting() throws Exception { AppBundle appBundle = 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 aa9e6ae9..b810b673 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 @@ -216,8 +216,9 @@ public void manifestIsMutated() throws Exception { .getValueAsDecimalInteger()) .isEqualTo(RuntimeEnabledSdkVersionEncoder.encodeSdkMajorAndMinorVersion(major, minor)); + // mutations. + assertThat(manifest.getSdkPatchVersionMetadata()).hasValue(patch); // mutations. - assertThat(manifest.getSdkPatchVersionProperty()).hasValue(patch); assertThat(manifest.getSdkProviderClassNameProperty()).hasValue(sdkProviderClassName); assertThat(manifest.getCompatSdkProviderClassNameProperty()).isEmpty(); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java index b25afcab..a37723ba 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java @@ -38,6 +38,7 @@ import com.android.bundle.Files.TargetedNativeDirectory; import com.android.bundle.SdkBundleConfigProto.SdkBundle; import com.android.bundle.SdkBundleConfigProto.SdkBundleConfig; +import com.android.bundle.SdkBundleConfigProto.SdkDependencyType; import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; import com.android.bundle.Targeting.Abi; @@ -99,7 +100,8 @@ public class BuildSdkBundleCommandTest { .setVersionMajor(123) .setVersionMinor(234) .setBuildTimeVersionPatch(345) - .setCertificateDigest(VALID_CERT_FINGERPRINT)) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setDependencyType(SdkDependencyType.SDK_DEPENDENCY_TYPE_REQUIRED)) .build(); private static final byte[] SDK_BUNDLE_CONFIG_CONTENTS = ("{ sdk_dependencies: [{ " @@ -109,7 +111,9 @@ public class BuildSdkBundleCommandTest { + "build_time_version_patch: 345, " + "certificate_digest: \"" + VALID_CERT_FINGERPRINT - + "\" }]}") + + "\", " + + "dependency_type: \"SDK_DEPENDENCY_TYPE_REQUIRED\"" + + "}]}") .getBytes(UTF_8); @Rule public final TemporaryFolder tmp = new TemporaryFolder(); diff --git a/src/test/java/com/android/tools/build/bundletool/device/GlExtensionsParserTest.java b/src/test/java/com/android/tools/build/bundletool/device/GlExtensionsParserTest.java index 10ca5010..25dc350f 100644 --- a/src/test/java/com/android/tools/build/bundletool/device/GlExtensionsParserTest.java +++ b/src/test/java/com/android/tools/build/bundletool/device/GlExtensionsParserTest.java @@ -17,9 +17,7 @@ package com.android.tools.build.bundletool.device; import static com.google.common.truth.Truth.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import com.android.tools.build.bundletool.model.exceptions.AdbOutputParseException; import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; @@ -75,14 +73,12 @@ public void outputWithMultipleGLES() { } @Test - public void invalidOutput_throws() { - ImmutableList dumpsysOutput = - ImmutableList.of("", "SurfaceFlinger global state:", "unrelated information", "that's all"); - Throwable exception = - assertThrows( - AdbOutputParseException.class, () -> new GlExtensionsParser().parse(dumpsysOutput)); - assertThat(exception) - .hasMessageThat() - .contains("Unexpected output of 'dumpsys SurfaceFlinger' command: no GL extensions found."); + public void invalidOutput_returnsEmpty() { + ImmutableList glExtensions = + new GlExtensionsParser() + .parse( + ImmutableList.of( + "", "SurfaceFlinger global state:", "unrelated information", "that's all")); + assertThat(glExtensions).isEmpty(); } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProviderTest.java b/src/test/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProviderTest.java index 608a7614..fb75df33 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProviderTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/DefaultSigningConfigurationProviderTest.java @@ -195,6 +195,37 @@ public void v3SigningRestrictedToRPlus_apkTargetingPreR_shouldNotSignWithV3Rotat assertThat(apksigSigningConfig.getSigningCertificateLineage()).isEmpty(); } + @Test + public void v3SigningRestrictedToRPlus_variantTargetingRPlus_shouldSignWithV3Rotation() { + SigningConfiguration signingConfig = + signingConfigForV3Rotation.toBuilder() + .setMinimumV3RotationApiVersion(Optional.of(ANDROID_R_API_VERSION)) + .build(); + SigningConfigurationProvider signingConfigProvider = + new DefaultSigningConfigurationProvider( + signingConfig, BundleToolVersion.getCurrentVersion()); + ModuleSplit split = + createModuleSplitWithMinSdk(ANDROID_Q_API_VERSION).toBuilder() + .setVariantTargeting( + VariantTargeting.newBuilder() + .setSdkVersionTargeting( + SdkVersionTargeting.newBuilder() + .addValue( + SdkVersion.newBuilder() + .setMin( + Int32Value.newBuilder().setValue(ANDROID_R_API_VERSION)))) + .build()) + .build(); + + ApksigSigningConfiguration apksigSigningConfig = + signingConfigProvider.getSigningConfiguration(ApkDescription.fromModuleSplit(split)); + + assertThat(apksigSigningConfig.getSignerConfigs()) + .containsExactly(oldSignerConfig, signerConfig) + .inOrder(); + assertThat(apksigSigningConfig.getSigningCertificateLineage()).hasValue(lineage); + } + private static ModuleSplit createModuleSplitWithMinSdk(int minSdkVersion) { return createModuleSplit().toBuilder() .setAndroidManifest( diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java index 398904f0..d1453d8b 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java @@ -28,6 +28,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.HAS_CODE_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.INCLUDE_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_TIME_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.INTENT_FILTER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_FEATURE_SPLIT_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_RESOURCE_ID; @@ -48,6 +49,8 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SANDBOX_VERSION_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.THEME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.TOOLS_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_FEATURE_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.USES_SDK_LIBRARY_ELEMENT_NAME; @@ -1421,6 +1424,55 @@ public void addManifestChildElement() { "featureName"))))); } + @Test + public void setActivityTheme_succeeds() { + XmlNode category = + xmlNode( + xmlElement( + CATEGORY_ELEMENT_NAME, + ImmutableList.of( + xmlAttribute( + ANDROID_NAMESPACE_URI, + NAME_ATTRIBUTE_NAME, + NAME_RESOURCE_ID, + "myCategory")))); + XmlNode intentFilter = xmlNode(xmlElement(INTENT_FILTER_ELEMENT_NAME, category)); + XmlNode activity = + xmlNode( + xmlElement( + ACTIVITY_ELEMENT_NAME, + ImmutableList.of( + xmlAttribute( + ANDROID_NAMESPACE_URI, + NAME_ATTRIBUTE_NAME, + NAME_RESOURCE_ID, + "myActivity")), + intentFilter)); + XmlNode application = xmlNode(xmlElement(APPLICATION_ELEMENT_NAME, activity)); + AndroidManifest androidManifest = + AndroidManifest.create(xmlNode(xmlElement("manifest", application))); + + AndroidManifest edited = + androidManifest.toEditor().setActivityTheme("myActivity", "myCategory", 1).save(); + + XmlNode expectedActivity = + xmlNode( + xmlElement( + ACTIVITY_ELEMENT_NAME, + ImmutableList.of( + xmlAttribute( + ANDROID_NAMESPACE_URI, NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID, "myActivity"), + xmlResourceReferenceAttribute( + ANDROID_NAMESPACE_URI, THEME_ATTRIBUTE_NAME, THEME_RESOURCE_ID, 1)), + intentFilter)); + AndroidManifest expected = + AndroidManifest.create( + xmlNode( + xmlElement( + "manifest", xmlNode(xmlElement(APPLICATION_ELEMENT_NAME, expectedActivity))))); + assertThat(edited).isEqualTo(expected); + } + private static void assertUsesSdkLibraryAttributes( XmlElement usesSdkLibraryElement, String name, long versionMajor, String certDigest) { assertThat(usesSdkLibraryElement.getAttributeList()).hasSize(3); diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectorySegmentTest.java index 5e326dd6..30bfa518 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 @@ -16,18 +16,20 @@ package com.android.tools.build.bundletool.model.targeting; +import static com.android.tools.build.bundletool.model.targeting.TargetedDirectorySegment.constructTargetingSegmentPath; 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; 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 org.junit.jupiter.api.Assertions.assertThrows; +import com.android.bundle.Targeting.AssetsDirectoryTargeting; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -39,7 +41,7 @@ public class TargetedDirectorySegmentTest { public void testTargeting_nokey_value_ok() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()).isEmpty(); + assertThat(segment.getTargetingDimensions()).isEmpty(); assertThat(segment.getTargeting()).isEqualToDefaultInstance(); } @@ -47,8 +49,8 @@ public void testTargeting_nokey_value_ok() { public void testTargeting_tcf_astc() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_astc"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -59,8 +61,8 @@ public void testTargeting_tcf_astc() { public void testTargeting_tcf_atc() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_atc"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -71,8 +73,8 @@ public void testTargeting_tcf_atc() { public void testTargeting_tcf_dxt1() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_dxt1"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -83,8 +85,8 @@ public void testTargeting_tcf_dxt1() { public void testTargeting_tcf_latc() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_latc"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -95,8 +97,8 @@ public void testTargeting_tcf_latc() { public void testTargeting_tcf_paletted() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_paletted"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -107,8 +109,8 @@ public void testTargeting_tcf_paletted() { public void testTargeting_tcf_pvrtc() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_pvrtc"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -119,8 +121,8 @@ public void testTargeting_tcf_pvrtc() { public void testTargeting_tcf_etc1() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_etc1"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -131,8 +133,8 @@ public void testTargeting_tcf_etc1() { public void testTargeting_tcf_etc2() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_etc2"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -143,8 +145,8 @@ public void testTargeting_tcf_etc2() { public void testTargeting_tcf_s3tc() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_s3tc"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -155,8 +157,8 @@ public void testTargeting_tcf_s3tc() { public void testTargeting_tcf_3dc() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tcf_3dc"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()) - .hasValue(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); + assertThat(segment.getTargetingDimensions()) + .contains(TargetingDimension.TEXTURE_COMPRESSION_FORMAT); assertThat(segment.getTargeting()) .isEqualTo( assetsDirectoryTargeting( @@ -180,7 +182,7 @@ public void testTargeting_badKey() { public void testTargeting_deviceTier() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#tier_1"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.DEVICE_TIER); + assertThat(segment.getTargetingDimensions()).contains(TargetingDimension.DEVICE_TIER); assertThat(segment.getTargeting()).isEqualTo(assetsDirectoryTargeting(deviceTierTargeting(1))); } @@ -206,7 +208,7 @@ public void testTargeting_deviceTier_invalidTierName_notAnInt() { 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.getTargetingDimensions()).contains(TargetingDimension.COUNTRY_SET); assertThat(segment.getTargeting()) .isEqualTo(assetsDirectoryTargeting(countrySetTargeting("latam"))); } @@ -216,12 +218,12 @@ public void testTargeting_countrySet_invalidCountrySetName_withSpecialChars() { InvalidBundleException exception = assertThrows( InvalidBundleException.class, - () -> TargetedDirectorySegment.parse("assets/test#countries_latam$%@#")); + () -> 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'."); + + " 'latam$%@' for directory 'assets/test'."); } @Test @@ -254,7 +256,7 @@ public void testFailsParsing_missingValue() { public void testTargeting_language_ok() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#lang_en"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.LANGUAGE); + assertThat(segment.getTargetingDimensions()).contains(TargetingDimension.LANGUAGE); assertThat(segment.getTargeting()).isEqualTo(assetsDirectoryTargeting(languageTargeting("en"))); } @@ -262,7 +264,7 @@ public void testTargeting_language_ok() { public void testTargeting_languageThreeChars_ok() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#lang_fil"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.LANGUAGE); + assertThat(segment.getTargetingDimensions()).contains(TargetingDimension.LANGUAGE); assertThat(segment.getTargeting()) .isEqualTo(assetsDirectoryTargeting(languageTargeting("fil"))); } @@ -271,7 +273,7 @@ public void testTargeting_languageThreeChars_ok() { public void testTargeting_upperCase_OK() { TargetedDirectorySegment segment = TargetedDirectorySegment.parse("test#lang_FR"); assertThat(segment.getName()).isEqualTo("test"); - assertThat(segment.getTargetingDimension()).hasValue(TargetingDimension.LANGUAGE); + assertThat(segment.getTargetingDimensions()).contains(TargetingDimension.LANGUAGE); assertThat(segment.getTargeting()).isEqualTo(assetsDirectoryTargeting(languageTargeting("fr"))); } @@ -338,4 +340,112 @@ public void testTargeting_countrySet_toPathIdempotent() { segment = TargetedDirectorySegment.parse("test#countries_sea"); assertThat(segment.toPathSegment()).isEqualTo("test#countries_sea"); } + + @Test + public void testTargeting_nested_toPathIdempotent() { + TargetedDirectorySegment segment = + TargetedDirectorySegment.parse("test#countries_latam#tcf_astc"); + assertThat(segment.toPathSegment()).isEqualTo("test#countries_latam#tcf_astc"); + + segment = TargetedDirectorySegment.parse("test#tcf_astc#countries_latam"); + assertThat(segment.toPathSegment()).isEqualTo("test#tcf_astc#countries_latam"); + } + + @Test + public void testTargeting_nested_moreThanTwoDimension_throws() { + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> TargetedDirectorySegment.parse("test#countries_latam#tcf_astc#tier_2")); + assertThat(exception) + .hasMessageThat() + .contains( + "No directory should target more than two dimension. Found directory" + + " 'test#countries_latam#tcf_astc#tier_2' targeting more than two dimension."); + } + + @Test + public void testTargeting_nested_dimensionOtherThanTcfTierCountriesUsed_throws() { + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> TargetedDirectorySegment.parse("test#countries_latam#lang_en-US")); + assertThat(exception) + .hasMessageThat() + .contains( + "Targeting dimension 'LANGUAGE' should not be nested with other dimensions. Found" + + " directory 'test#countries_latam#lang_en-US' which nests the dimension with" + + " other dimensions."); + } + + @Test + public void testTargeting_invalidTargeting_throws() { + InvalidBundleException exception = + assertThrows(InvalidBundleException.class, () -> TargetedDirectorySegment.parse("#tier_2")); + assertThat(exception) + .hasMessageThat() + .contains( + "Cannot tokenize targeted directory '#tier_2'. Expecting either '' or" + + " '#_' format."); + } + + @Test + public void testTargeting_nested_sameDimensionUsedMultipleTimes_throws() { + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> TargetedDirectorySegment.parse("test#tier_2#tier_1")); + assertThat(exception) + .hasMessageThat() + .contains( + "No directory should be targeted more than once on the same dimension. Found directory" + + " 'test#tier_2#tier_1' targeted multiple times on same dimension."); + } + + @Test + public void testTargeting_nested_invalidKey_throws() { + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> TargetedDirectorySegment.parse("test#tier_2#invalid_astc")); + assertThat(exception) + .hasMessageThat() + .contains( + "Unrecognized key: 'invalid' used in targeting of directory" + + " 'test#tier_2#invalid_astc'."); + } + + @Test + public void constructTargetingSegmentPath_allDimensionsPresent() { + AssetsDirectoryTargeting targeting = + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .setTextureCompressionFormat( + textureCompressionTargeting(TextureCompressionFormatAlias.ASTC)) + .build(); + ImmutableList targetingOrder = + ImmutableList.of( + TargetingDimension.TEXTURE_COMPRESSION_FORMAT, TargetingDimension.COUNTRY_SET); + + assertThat(constructTargetingSegmentPath(targeting, targetingOrder)) + .isEqualTo("#tcf_astc#countries_latam"); + } + + @Test + public void constructTargetingSegmentPath_dimensionsMissing() { + AssetsDirectoryTargeting targeting = + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .setTextureCompressionFormat( + textureCompressionTargeting(TextureCompressionFormatAlias.ASTC)) + .build(); + ImmutableList targetingOrder = + ImmutableList.of( + TargetingDimension.TEXTURE_COMPRESSION_FORMAT, + TargetingDimension.COUNTRY_SET, + TargetingDimension.DEVICE_TIER); + + assertThat(constructTargetingSegmentPath(targeting, targetingOrder)) + .isEqualTo("#tcf_astc#countries_latam"); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectoryTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectoryTest.java index 81a72271..065a74ae 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectoryTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetedDirectoryTest.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.languageTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; import static com.google.common.truth.Truth.assertThat; @@ -163,4 +164,24 @@ public void duplicateDimensionsOnPath_throws() { TargetedDirectory.parse( ZipPath.create("assets/world/gfx#tcf_etc1/other/gl#opengl_2.0/texture#tcf_atc"))); } + + @Test + public void nestedTargeting_succeeds() { + ZipPath path = ZipPath.create("assets/world/texture#countries_latam#tcf_etc1"); + TargetedDirectory actual = TargetedDirectory.parse(path); + + assertThat(actual.getPathSegments()).hasSize(3); + assertThat(actual.getPathSegments().get(0).getName()).isEqualTo("assets"); + assertThat(actual.getPathSegments().get(0).getTargeting()).isEqualToDefaultInstance(); + assertThat(actual.getPathSegments().get(1).getName()).isEqualTo("world"); + assertThat(actual.getPathSegments().get(1).getTargeting()).isEqualToDefaultInstance(); + assertThat(actual.getPathSegments().get(2).getName()).isEqualTo("texture"); + assertThat(actual.getPathSegments().get(2).getTargeting()) + .isEqualTo( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .setTextureCompressionFormat( + textureCompressionTargeting(TextureCompressionFormatAlias.ETC1_RGB8)) + .build()); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java index 49a64e45..8930af62 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/targeting/TargetingGeneratorTest.java @@ -16,8 +16,12 @@ package com.android.tools.build.bundletool.model.targeting; +import static com.android.tools.build.bundletool.model.utils.TargetingNormalizer.normalizeAssetsDirectoryTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeCountrySetTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeLanguageTargeting; +import static com.android.tools.build.bundletool.testing.TargetingUtils.alternativeTextureCompressionTargeting; 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; @@ -163,7 +167,7 @@ public void generateTargetingForApexImages_createsAllTargeting() throws Exceptio checkState(allAbiFiles.size() > 1); // Otherwise this test is useless. ApexImages apexImages = - generator.generateTargetingForApexImages(allAbiFiles, /*hasBuildInfo=*/ true); + generator.generateTargetingForApexImages(allAbiFiles, /* hasBuildInfo= */ true); List images = apexImages.getImageList(); assertThat(images).hasSize(allAbiFiles.size()); @@ -177,7 +181,7 @@ public void generateTargetingForApexImages_abiBaseNamesDisallowed() throws Excep () -> generator.generateTargetingForApexImages( ImmutableList.of(ZipPath.create("x86.ARM64-v8a.img")), - /*hasBuildInfo=*/ false)); + /* hasBuildInfo= */ false)); assertThat(exception) .hasMessageThat() @@ -193,7 +197,8 @@ public void generateTargetingForApexImages_baseNameNotAnAbi_throws() throws Exce InvalidBundleException.class, () -> generator.generateTargetingForApexImages( - ImmutableList.of(ZipPath.create("non_abi_name.img")), /*hasBuildInfo=*/ false)); + ImmutableList.of(ZipPath.create("non_abi_name.img")), + /* hasBuildInfo= */ false)); assertThat(exception) .hasMessageThat() @@ -236,44 +241,6 @@ public void generateTargetingForAssets_nonTargetedDirectories() throws Exception .build()); } - @Test - public void generateTargetingForAssets_typeMismatch_leaf() throws Exception { - Throwable t = - assertThrows( - InvalidBundleException.class, - () -> - new TargetingGenerator() - .generateTargetingForAssets( - ImmutableList.of( - ZipPath.create("assets/world/gfx#lang_ru"), - ZipPath.create("assets/world/gfx#tcf_etc1")))); - assertThat(t) - .hasMessageThat() - .contains( - "Expected at most one dimension type used for targeting of 'assets/world/gfx'. " - + "However, the following dimensions were used: " - + "'LANGUAGE', 'TEXTURE_COMPRESSION_FORMAT'."); - } - - @Test - public void generateTargetingForAssets_typeMismatch_midPath() throws Exception { - Throwable t = - assertThrows( - InvalidBundleException.class, - () -> - new TargetingGenerator() - .generateTargetingForAssets( - ImmutableList.of( - ZipPath.create("assets/world/texture#tcf_etc1/gfx#lang_en"), - ZipPath.create("assets/world/texture#lang_ru/gfx#tcf_etc1")))); - assertThat(t) - .hasMessageThat() - .contains( - "Expected at most one dimension type used for targeting of 'assets/world/texture'. " - + "However, the following dimensions were used: " - + "'LANGUAGE', 'TEXTURE_COMPRESSION_FORMAT'."); - } - @Test public void generateTargetingForAssets_different_types_leaves_ok() throws Exception { Assets assetsConfig = @@ -652,4 +619,106 @@ public void generateTargetingForAssets_assetsAtTopLevel() throws Exception { .setTargeting(AssetsDirectoryTargeting.getDefaultInstance())) .build()); } + + @Test + public void generateTargetingForAssets_nestedTargeting() { + ImmutableList assetDirectories = + ImmutableList.of( + ZipPath.create("assets/img#countries_latam#tcf_astc"), + ZipPath.create("assets/img#countries_latam#tcf_pvrtc"), + ZipPath.create("assets/img#countries_latam"), + ZipPath.create("assets/img#tcf_astc"), + ZipPath.create("assets/img#tcf_pvrtc"), + ZipPath.create("assets/img")); + + Assets assetsConfig = new TargetingGenerator().generateTargetingForAssets(assetDirectories); + + assertThat(assetsConfig) + .ignoringRepeatedFieldOrder() + .isEqualTo( + Assets.newBuilder() + .addDirectory( + TargetedAssetsDirectory.newBuilder() + .setPath("assets/img#countries_latam#tcf_astc") + .setTargeting( + normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .setTextureCompressionFormat( + textureCompressionTargeting( + /* value= */ TextureCompressionFormatAlias.ASTC, + /* alternatives= */ ImmutableSet.of( + TextureCompressionFormatAlias.PVRTC))) + .build()))) + .addDirectory( + TargetedAssetsDirectory.newBuilder() + .setPath("assets/img#countries_latam#tcf_pvrtc") + .setTargeting( + normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .setTextureCompressionFormat( + textureCompressionTargeting( + /* value= */ TextureCompressionFormatAlias.PVRTC, + /* alternatives= */ ImmutableSet.of( + TextureCompressionFormatAlias.ASTC))) + .build()))) + .addDirectory( + TargetedAssetsDirectory.newBuilder() + .setPath("assets/img#countries_latam") + .setTargeting( + normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .setTextureCompressionFormat( + alternativeTextureCompressionTargeting( + TextureCompressionFormatAlias.PVRTC, + TextureCompressionFormatAlias.ASTC)) + .build()))) + .addDirectory( + TargetedAssetsDirectory.newBuilder() + .setPath("assets/img#tcf_astc") + .setTargeting( + normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet( + alternativeCountrySetTargeting( + /* alternatives= */ ImmutableList.of("latam"))) + .setTextureCompressionFormat( + textureCompressionTargeting( + /* value= */ TextureCompressionFormatAlias.ASTC, + /* alternatives= */ ImmutableSet.of( + TextureCompressionFormatAlias.PVRTC))) + .build()))) + .addDirectory( + TargetedAssetsDirectory.newBuilder() + .setPath("assets/img#tcf_pvrtc") + .setTargeting( + normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet( + alternativeCountrySetTargeting( + /* alternatives= */ ImmutableList.of("latam"))) + .setTextureCompressionFormat( + textureCompressionTargeting( + /* value= */ TextureCompressionFormatAlias.PVRTC, + /* alternatives= */ ImmutableSet.of( + TextureCompressionFormatAlias.ASTC))) + .build()))) + .addDirectory( + TargetedAssetsDirectory.newBuilder() + .setPath("assets/img") + .setTargeting( + normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet( + alternativeCountrySetTargeting( + /* alternatives= */ ImmutableList.of("latam"))) + .setTextureCompressionFormat( + alternativeTextureCompressionTargeting( + TextureCompressionFormatAlias.PVRTC, + TextureCompressionFormatAlias.ASTC)) + .build()))) + .build()); + } } 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 9f5ba360..25c5011b 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 @@ -16,9 +16,15 @@ package com.android.tools.build.bundletool.model.targeting; +import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getAlternativeTargeting; import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getMaxSdk; import static com.android.tools.build.bundletool.model.targeting.TargetingUtils.getMinSdk; +import static com.android.tools.build.bundletool.model.utils.TargetingNormalizer.normalizeAssetsDirectoryTargeting; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; 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.alternativeTextureCompressionTargeting; 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; @@ -27,15 +33,25 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionFrom; import static com.android.tools.build.bundletool.testing.TargetingUtils.sdkVersionTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.textureCompressionTargeting; +import static com.google.common.collect.ImmutableList.toImmutableList; 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 com.android.bundle.Targeting.Abi.AbiAlias; import com.android.bundle.Targeting.AssetsDirectoryTargeting; +import com.android.bundle.Targeting.CountrySetTargeting; import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.SdkVersionTargeting; +import com.android.bundle.Targeting.TextureCompressionFormat; import com.android.bundle.Targeting.TextureCompressionFormat.TextureCompressionFormatAlias; +import com.android.bundle.Targeting.TextureCompressionFormatTargeting; import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; import org.junit.Test; @@ -212,6 +228,295 @@ public void maxSdk_nonEmptySdkTargeting() { .isEqualTo(Integer.MAX_VALUE); } + @Test + public void extractDimensionTargeting_countrySetTexture() { + AssetsDirectoryTargeting targeting = + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .setTextureCompressionFormat( + textureCompressionTargeting(TextureCompressionFormatAlias.ASTC)) + .setDeviceTier(deviceTierTargeting(2)) + .build(); + + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.COUNTRY_SET)) + .hasValue( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet(countrySetTargeting("latam")) + .build()); + assertThat( + TargetingUtils.extractDimensionTargeting( + targeting, TargetingDimension.TEXTURE_COMPRESSION_FORMAT)) + .hasValue( + AssetsDirectoryTargeting.newBuilder() + .setTextureCompressionFormat( + textureCompressionTargeting(TextureCompressionFormatAlias.ASTC)) + .build()); + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.DEVICE_TIER)) + .hasValue( + AssetsDirectoryTargeting.newBuilder().setDeviceTier(deviceTierTargeting(2)).build()); + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.ABI)) + .isEmpty(); + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.LANGUAGE)) + .isEmpty(); + } + + @Test + public void extractDimensionTargeting_empty() { + AssetsDirectoryTargeting targeting = AssetsDirectoryTargeting.getDefaultInstance(); + + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.COUNTRY_SET)) + .isEmpty(); + assertThat( + TargetingUtils.extractDimensionTargeting( + targeting, TargetingDimension.TEXTURE_COMPRESSION_FORMAT)) + .isEmpty(); + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.DEVICE_TIER)) + .isEmpty(); + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.ABI)) + .isEmpty(); + assertThat(TargetingUtils.extractDimensionTargeting(targeting, TargetingDimension.LANGUAGE)) + .isEmpty(); + } + + @Test + public void getAssetDirectories() { + BundleModule module = + new BundleModuleBuilder("a") + .addFile("assets/img1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/img1#tcf_astc/image.jpg") + .addFile("assets/img1#tcf_pvrtc/image.jpg") + .addFile("assets/img1/image.jpg") + .addFile("assets/file.txt") + .addFile("foo/bar/file.txt") + .setManifest(androidManifest("com.test.app")) + .build(); + + assertThat(TargetingUtils.getAssetDirectories(module)) + .containsExactly( + ZipPath.create("assets/img1#countries_latam#tcf_astc"), + ZipPath.create("assets/img1#tcf_astc"), + ZipPath.create("assets/img1#tcf_pvrtc"), + ZipPath.create("assets/img1"), + ZipPath.create("assets")); + } + + @Test + public void getAlternativeTargeting_withSingleDimensionTargeting_countrySet() { + AssetsDirectoryTargeting latamTargeting = + assetsDirectoryTargeting(countrySetTargeting("latam")); + AssetsDirectoryTargeting seaTargeting = assetsDirectoryTargeting(countrySetTargeting("sea")); + AssetsDirectoryTargeting fallbackTargeting = AssetsDirectoryTargeting.getDefaultInstance(); + + assertThat( + getAlternativeTargeting( + /* targeting= */ latamTargeting, + /* allTargeting= */ ImmutableList.of( + latamTargeting, seaTargeting, fallbackTargeting))) + .isEqualTo( + normalizeAssetsDirectoryTargeting( + assetsDirectoryTargeting( + alternativeCountrySetTargeting(/* alternatives= */ ImmutableList.of("sea"))))); + assertThat( + getAlternativeTargeting( + /* targeting= */ fallbackTargeting, + /* allTargeting= */ ImmutableList.of( + latamTargeting, seaTargeting, fallbackTargeting))) + .isEqualTo( + normalizeAssetsDirectoryTargeting( + assetsDirectoryTargeting( + alternativeCountrySetTargeting( + /* alternatives= */ ImmutableList.of("sea", "latam"))))); + } + + @Test + public void getAlternativeTargeting_withSingleDimensionTargeting_deviceTier() { + AssetsDirectoryTargeting tier0Targeting = assetsDirectoryTargeting(deviceTierTargeting(0)); + AssetsDirectoryTargeting tier1Targeting = assetsDirectoryTargeting(deviceTierTargeting(1)); + AssetsDirectoryTargeting tier2Targeting = assetsDirectoryTargeting(deviceTierTargeting(2)); + + assertThat( + getAlternativeTargeting( + /* targeting= */ tier2Targeting, + /* allTargeting= */ ImmutableList.of( + tier0Targeting, tier1Targeting, tier2Targeting))) + .isEqualTo( + normalizeAssetsDirectoryTargeting( + assetsDirectoryTargeting( + alternativeDeviceTierTargeting(/* alternatives= */ ImmutableList.of(0, 1))))); + assertThat( + getAlternativeTargeting( + /* targeting= */ tier0Targeting, + /* allTargeting= */ ImmutableList.of( + tier0Targeting, tier1Targeting, tier2Targeting))) + .isEqualTo( + normalizeAssetsDirectoryTargeting( + assetsDirectoryTargeting( + alternativeDeviceTierTargeting(/* alternatives= */ ImmutableList.of(1, 2))))); + } + + @Test + public void getAlternativeTargeting_withSingleDimensionTargeting_tcf() { + AssetsDirectoryTargeting astcTargeting = + assetsDirectoryTargeting(textureCompressionTargeting(TextureCompressionFormatAlias.ASTC)); + AssetsDirectoryTargeting pvrtcTargeting = + assetsDirectoryTargeting(textureCompressionTargeting(TextureCompressionFormatAlias.PVRTC)); + AssetsDirectoryTargeting fallbackTargeting = AssetsDirectoryTargeting.getDefaultInstance(); + + assertThat( + getAlternativeTargeting( + /* targeting= */ astcTargeting, + /* allTargeting= */ ImmutableList.of( + astcTargeting, pvrtcTargeting, fallbackTargeting))) + .isEqualTo( + normalizeAssetsDirectoryTargeting( + assetsDirectoryTargeting( + alternativeTextureCompressionTargeting(TextureCompressionFormatAlias.PVRTC)))); + assertThat( + getAlternativeTargeting( + /* targeting= */ fallbackTargeting, + /* allTargeting= */ ImmutableList.of( + astcTargeting, pvrtcTargeting, fallbackTargeting))) + .isEqualTo( + normalizeAssetsDirectoryTargeting( + assetsDirectoryTargeting( + alternativeTextureCompressionTargeting( + TextureCompressionFormatAlias.ASTC, TextureCompressionFormatAlias.PVRTC)))); + } + + @Test + public void getAlternativeTargeting_withNestedDimensionTargeting_countrySetAndTcf() { + AssetsDirectoryTargeting latamAstcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of("latam"), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.ASTC)); + AssetsDirectoryTargeting latamPvrtcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of("latam"), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.PVRTC)); + AssetsDirectoryTargeting latamTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of("latam"), /* tcfValue= */ ImmutableList.of()); + AssetsDirectoryTargeting astcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of(), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.ASTC)); + AssetsDirectoryTargeting pvrtcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of(), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.PVRTC)); + AssetsDirectoryTargeting fallbackTargeting = AssetsDirectoryTargeting.getDefaultInstance(); + ImmutableList allTargeting = + ImmutableList.of( + latamAstcTargeting, + latamPvrtcTargeting, + latamTargeting, + astcTargeting, + pvrtcTargeting, + fallbackTargeting); + + assertThat(getAlternativeTargeting(latamAstcTargeting, allTargeting)) + .isEqualTo( + AssetsDirectoryTargeting.newBuilder() + .setTextureCompressionFormat( + TextureCompressionFormatTargeting.newBuilder() + .addAlternatives( + TextureCompressionFormat.newBuilder() + .setAlias(TextureCompressionFormatAlias.PVRTC))) + .build()); + assertThat(getAlternativeTargeting(fallbackTargeting, allTargeting)) + .isEqualTo( + normalizeAssetsDirectoryTargeting( + AssetsDirectoryTargeting.newBuilder() + .setCountrySet( + alternativeCountrySetTargeting( + /* alternatives= */ ImmutableList.of("latam"))) + .setTextureCompressionFormat( + alternativeTextureCompressionTargeting( + TextureCompressionFormatAlias.ASTC, + TextureCompressionFormatAlias.PVRTC)) + .build())); + } + + @Test + public void getAssetsDirectoryTargetingByDimension() { + AssetsDirectoryTargeting latamAstcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of("latam"), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.ASTC)); + AssetsDirectoryTargeting latamPvrtcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of("latam"), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.PVRTC)); + AssetsDirectoryTargeting latamTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of("latam"), /* tcfValue= */ ImmutableList.of()); + AssetsDirectoryTargeting astcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of(), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.ASTC)); + AssetsDirectoryTargeting pvrtcTargeting = + constructNestedTargetingWithCountrySetAndTcf( + /* countrySetValue= */ ImmutableList.of(), + /* tcfValue= */ ImmutableList.of(TextureCompressionFormatAlias.PVRTC)); + AssetsDirectoryTargeting fallbackTargeting = AssetsDirectoryTargeting.getDefaultInstance(); + ImmutableList allTargeting = + ImmutableList.of( + latamAstcTargeting, + latamPvrtcTargeting, + latamTargeting, + astcTargeting, + pvrtcTargeting, + fallbackTargeting); + ImmutableMultimap + expectedAssetsDirectoryTargetingByDimension = + ImmutableMultimap.builder() + .put( + TargetingDimension.COUNTRY_SET, + assetsDirectoryTargeting(countrySetTargeting("latam"))) + .put(TargetingDimension.COUNTRY_SET, AssetsDirectoryTargeting.getDefaultInstance()) + .put( + TargetingDimension.TEXTURE_COMPRESSION_FORMAT, + assetsDirectoryTargeting( + textureCompressionTargeting(TextureCompressionFormatAlias.ASTC))) + .put( + TargetingDimension.TEXTURE_COMPRESSION_FORMAT, + assetsDirectoryTargeting( + textureCompressionTargeting(TextureCompressionFormatAlias.PVRTC))) + .put( + TargetingDimension.TEXTURE_COMPRESSION_FORMAT, + AssetsDirectoryTargeting.getDefaultInstance()) + .build(); + + ImmutableMultimap + assetsDirectoryTargetingByDimension = + TargetingUtils.getAssetsDirectoryTargetingByDimension(allTargeting); + + assertThat(assetsDirectoryTargetingByDimension) + .isEqualTo(expectedAssetsDirectoryTargetingByDimension); + } + + private static AssetsDirectoryTargeting constructNestedTargetingWithCountrySetAndTcf( + ImmutableList countrySetValue, + ImmutableList tcfValue) { + AssetsDirectoryTargeting.Builder targetingBuilder = AssetsDirectoryTargeting.newBuilder(); + if (!countrySetValue.isEmpty()) { + targetingBuilder.setCountrySet( + CountrySetTargeting.newBuilder().addAllValue(countrySetValue).build()); + } + if (!tcfValue.isEmpty()) { + targetingBuilder.setTextureCompressionFormat( + TextureCompressionFormatTargeting.newBuilder() + .addAllValue( + tcfValue.stream() + .map( + tcfAlias -> + TextureCompressionFormat.newBuilder().setAlias(tcfAlias).build()) + .collect(toImmutableList())) + .build()); + } + return targetingBuilder.build(); + } + private static VariantTargeting variantTargetingFromSdkVersion(SdkVersion values) { return variantTargetingFromSdkVersion(values, ImmutableSet.of()); } diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java index 5c712bb5..b708aee8 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizerTest.java @@ -27,6 +27,7 @@ import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import com.android.bundle.Targeting.ApkTargeting; +import com.android.bundle.Targeting.AssetsDirectoryTargeting; import com.android.bundle.Targeting.ScreenDensityTargeting; import com.android.bundle.Targeting.VariantTargeting; import com.android.tools.build.bundletool.testing.ProtoFuzzer; @@ -106,4 +107,21 @@ public void normalizeVariantTargeting_allTargetingDimensionsAreHandled() { assertThat(TargetingNormalizer.normalizeVariantTargeting(variantTargeting)) .isEqualTo(TargetingNormalizer.normalizeVariantTargeting(shuffledVariantTargeting)); } + + @Test + public void normalizeAssetsDirectoryTargeting_allTargetingDimensionsAreHandled() { + AssetsDirectoryTargeting assetsDirectoryTargeting = + ProtoFuzzer.randomProtoMessage(AssetsDirectoryTargeting.class); + AssetsDirectoryTargeting shuffledAssetsDirectoryTargeting = + ProtoFuzzer.shuffleRepeatedFields(assetsDirectoryTargeting); + // Sanity-check that the testing data was generated alright. + assertThat(assetsDirectoryTargeting).isNotEqualTo(shuffledAssetsDirectoryTargeting); + + // The following check fails, if the normalizing logic forgets to handle some dimension. + // This would typically happen when the targeting proto is extended by a new dimension. + assertThat(TargetingNormalizer.normalizeAssetsDirectoryTargeting(assetsDirectoryTargeting)) + .isEqualTo( + TargetingNormalizer.normalizeAssetsDirectoryTargeting( + shuffledAssetsDirectoryTargeting)); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java index d5206f0e..5ea18aea 100644 --- a/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/shards/StandaloneApksGeneratorTest.java @@ -35,6 +35,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlAttribute; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.HDPI; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.LDPI; +import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.MDPI; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.USER_PACKAGE_OFFSET; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.entry; import static com.android.tools.build.bundletool.testing.ResourcesTableFactory.fileReference; @@ -60,6 +61,7 @@ import static com.android.tools.build.bundletool.testing.truth.resources.TruthResourceTable.assertThat; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.MoreCollectors.onlyElement; 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; @@ -76,6 +78,7 @@ import com.android.bundle.Config.SplitsConfig; import com.android.bundle.Config.StandaloneConfig; import com.android.bundle.Config.StandaloneConfig.DexMergingStrategy; +import com.android.bundle.Config.StandaloneConfig.FeatureModulesMode; import com.android.bundle.Config.SuffixStripping; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; @@ -89,6 +92,7 @@ import com.android.tools.build.bundletool.model.AppBundle; import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ModuleSplit; import com.android.tools.build.bundletool.model.ModuleSplit.SplitType; import com.android.tools.build.bundletool.model.OptimizationDimension; @@ -97,6 +101,7 @@ import com.android.tools.build.bundletool.optimizations.ApkOptimizations; import com.android.tools.build.bundletool.splitters.RuntimeEnabledSdkTableInjector; import com.android.tools.build.bundletool.testing.AppBundleBuilder; +import com.android.tools.build.bundletool.testing.BundleConfigBuilder; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.ResourceTableBuilder; import com.android.tools.build.bundletool.testing.TestModule; @@ -1095,6 +1100,211 @@ public void manyModulesShardByNoDimension_producesFatApk() throws Exception { .onlyWithConfigs(LDPI); } + @Test + public void manyModulesShardByNoDimension_standaloneFeatureModules_producesTwoApk() + throws Exception { + BundleModule baseModule = + new BundleModuleBuilder("base") + .addFile("lib/x86_64/libtest1.so") + .addFile("lib/x86/libtest1.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86_64", nativeDirectoryTargeting(X86_64)), + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) + .addFile("res/drawable-ldpi/image1.jpg") + .setResourceTable( + resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "com.test.app", + type( + 0x01, + "drawable", + entry( + 0x01, + "image1", + fileReference("res/drawable-ldpi/image1.jpg", LDPI)))))) + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule featureModule = + new BundleModuleBuilder("feature") + .addFile("lib/x86_64/libtest2.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86_64", nativeDirectoryTargeting(X86_64)))) + .addFile("res/drawable-ldpi/image2.jpg") + .setResourceTable( + resourceTable( + pkg( + USER_PACKAGE_OFFSET + 1, + "com.test.app.split", + type( + 0x01, + "drawable", + entry( + 0x01, + "image2", + fileReference("res/drawable-ldpi/image2.jpg", LDPI)))))) + .setManifest(androidManifestForFeature("com.test.app")) + .build(); + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.SEPARATE_FEATURE_MODULES) + .build()) + .addModule(baseModule) + .addModule(featureModule) + .build(); + TestComponent.useTestModule(this, TestModule.builder().withAppBundle(appBundle).build()); + + ImmutableList shards = + standaloneApksGenerator.generateStandaloneApks( + ImmutableList.of(baseModule, featureModule), NO_DIMENSIONS); + + assertThat(shards).hasSize(2); + ModuleSplit baseApk = shards.get(0); + assertThat(extractPaths(baseApk.getEntries())) + .containsExactly( + "lib/x86_64/libtest1.so", "lib/x86/libtest1.so", "res/drawable-ldpi/image1.jpg"); + assertThat(baseApk.getResourceTable().get()) + .containsResource("com.test.app:drawable/image1") + .onlyWithConfigs(LDPI); + ModuleSplit featureApk = shards.get(1); + assertThat(extractPaths(featureApk.getEntries())) + .containsExactly("lib/x86_64/libtest2.so", "res/drawable-ldpi/image2.jpg"); + assertThat(featureApk.getResourceTable().get()) + .containsResource("com.test.app.split:drawable/image2") + .onlyWithConfigs(LDPI); + } + + @Test + public void + manyModulesShardByAbiDensity_standaloneFeatureModules_producesTwoApksPerEachAbiAndDensity() + throws Exception { + BundleModule baseModule = + new BundleModuleBuilder("base") + .addFile("lib/x86_64/libtest1.so") + .addFile("lib/x86/libtest1.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86_64", nativeDirectoryTargeting(X86_64)), + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) + .addFile("res/drawable-ldpi/image1.jpg") + .addFile("res/drawable-mdpi/image1.jpg") + .setResourceTable( + resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "com.test.app", + type( + 0x01, + "drawable", + entry( + 0x01, + "image1", + fileReference("res/drawable-ldpi/image1.jpg", LDPI), + fileReference("res/drawable-mdpi/image1.jpg", MDPI)))))) + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule featureModule = + new BundleModuleBuilder("feature") + .addFile("lib/x86_64/libtest2.so") + .addFile("lib/x86/libtest2.so") + .setNativeConfig( + nativeLibraries( + targetedNativeDirectory("lib/x86_64", nativeDirectoryTargeting(X86_64)), + targetedNativeDirectory("lib/x86", nativeDirectoryTargeting(X86)))) + .addFile("res/drawable-ldpi/image2.jpg") + .setResourceTable( + resourceTable( + pkg( + USER_PACKAGE_OFFSET + 1, + "com.test.app.split", + type( + 0x01, + "drawable", + entry( + 0x01, + "image2", + fileReference("res/drawable-ldpi/image2.jpg", LDPI)))))) + .setManifest(androidManifestForFeature("com.test.app")) + .build(); + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.SEPARATE_FEATURE_MODULES) + .build()) + .addModule(baseModule) + .addModule(featureModule) + .build(); + TestComponent.useTestModule(this, TestModule.builder().withAppBundle(appBundle).build()); + + ImmutableList shards = + standaloneApksGenerator.generateStandaloneApks( + ImmutableList.of(baseModule, featureModule), + standaloneApkOptimizations( + OptimizationDimension.ABI, OptimizationDimension.SCREEN_DENSITY)); + + assertThat(shards).hasSize(28); // 2 modules each in 2 ABIs and 7 DPIs. + + ApkTargeting x86Targeting = apkAbiTargeting(X86, ImmutableSet.of(X86_64)); + ApkTargeting x64Targeting = apkAbiTargeting(X86_64, ImmutableSet.of(X86)); + for (BundleModuleName moduleName : + ImmutableList.of(baseModule.getName(), featureModule.getName())) { + assertThat( + shards.stream() + .filter(split -> split.getModuleName().equals(moduleName)) + .map(ModuleSplit::getApkTargeting)) + .containsExactly( + mergeApkTargeting(x86Targeting, LDPI_TARGETING), + mergeApkTargeting(x86Targeting, MDPI_TARGETING), + mergeApkTargeting(x86Targeting, HDPI_TARGETING), + mergeApkTargeting(x86Targeting, XHDPI_TARGETING), + mergeApkTargeting(x86Targeting, XXHDPI_TARGETING), + mergeApkTargeting(x86Targeting, XXXHDPI_TARGETING), + mergeApkTargeting(x86Targeting, TVDPI_TARGETING), + mergeApkTargeting(x64Targeting, LDPI_TARGETING), + mergeApkTargeting(x64Targeting, MDPI_TARGETING), + mergeApkTargeting(x64Targeting, HDPI_TARGETING), + mergeApkTargeting(x64Targeting, XHDPI_TARGETING), + mergeApkTargeting(x64Targeting, XXHDPI_TARGETING), + mergeApkTargeting(x64Targeting, XXXHDPI_TARGETING), + mergeApkTargeting(x64Targeting, TVDPI_TARGETING)); + } + ModuleSplit baseX86Mdpi = + shards.stream() + .filter( + moduleSplit -> + moduleSplit.isBaseModuleSplit() + && moduleSplit + .getApkTargeting() + .equals(mergeApkTargeting(x86Targeting, MDPI_TARGETING))) + .collect(onlyElement()); + assertThat(extractPaths(baseX86Mdpi.getEntries())) + .containsExactly("lib/x86/libtest1.so", "res/drawable-mdpi/image1.jpg"); + assertThat(baseX86Mdpi.getResourceTable().get()) + .containsResource("com.test.app:drawable/image1") + .onlyWithConfigs(MDPI); + + ModuleSplit featureX86Mdpi = + shards.stream() + .filter( + moduleSplit -> + !moduleSplit.isBaseModuleSplit() + && moduleSplit + .getApkTargeting() + .equals(mergeApkTargeting(x86Targeting, MDPI_TARGETING))) + .collect(onlyElement()); + assertThat(extractPaths(featureX86Mdpi.getEntries())) + .containsExactly("lib/x86/libtest2.so", "res/drawable-ldpi/image2.jpg"); + // Here we check LDPI resource in MDPI shard because this resource does not have MDPI version. + assertThat(featureX86Mdpi.getResourceTable().get()) + .containsResource("com.test.app.split:drawable/image2") + .onlyWithConfigs(LDPI); + } + @Test public void manyModulesShardByNoDimension_producesFatApk_withTransparency() throws Exception { TestComponent.useTestModule( diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjectorTest.java index 208bd4ad..82912ce9 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/RuntimeEnabledSdkTableInjectorTest.java @@ -244,10 +244,12 @@ public void appBundleHasSdkDependencies_runtimeEnabledSdkTableAdded_splitVariant "", " ", " com.test.sdk1", + " 10002", " RuntimeEnabledSdk-com.test.sdk1/CompatSdkConfig.xml", " ", " ", " com.test.sdk2", + " 30004", " RuntimeEnabledSdk-com.test.sdk2/CompatSdkConfig.xml", " ", ""); @@ -313,10 +315,12 @@ public void appBundleHasSdkDependencies_runtimeEnabledSdkTableAdded_standaloneVa "", " ", " com.test.sdk1", + " 10002", " RuntimeEnabledSdk-com.test.sdk1/CompatSdkConfig.xml", " ", " ", " com.test.sdk2", + " 30004", " RuntimeEnabledSdk-com.test.sdk2/CompatSdkConfig.xml", " ", ""); diff --git a/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java b/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java index 96c79dfd..12068697 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/BundleConfigBuilder.java @@ -23,6 +23,7 @@ import com.android.bundle.Config.ResourceOptimizations.SparseEncoding; import com.android.bundle.Config.SplitDimension; import com.android.bundle.Config.StandaloneConfig.DexMergingStrategy; +import com.android.bundle.Config.StandaloneConfig.FeatureModulesMode; import com.android.bundle.Config.SuffixStripping; import com.android.bundle.Config.UncompressDexFiles.UncompressedDexTargetSdk; import com.android.bundle.Config.UnsignedEmbeddedApkConfig; @@ -179,6 +180,15 @@ public BundleConfigBuilder setVersion(String versionString) { return this; } + public BundleConfigBuilder setFeatureModulesModeForStandalone( + FeatureModulesMode featureModulesMode) { + builder + .getOptimizationsBuilder() + .getStandaloneConfigBuilder() + .setFeatureModulesMode(featureModulesMode); + return this; + } + public BundleConfig build() { return builder.build(); } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/FakeAsset.java b/src/test/java/com/android/tools/build/bundletool/testing/FakeAsset.java new file mode 100644 index 00000000..796b3e93 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/FakeAsset.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.testing; + +/** Fake representation of an asset in an SDK bundle for tests */ +public class FakeAsset { + private final String assetName; + private final byte[] assetContent; + + public FakeAsset(String assetName, byte[] assetContent) { + this.assetName = assetName; + this.assetContent = assetContent; + } + + public String getAssetName() { + return assetName; + } + + public byte[] getAssetContent() { + return assetContent; + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/FakeSigningConfigurationProvider.java b/src/test/java/com/android/tools/build/bundletool/testing/FakeSigningConfigurationProvider.java new file mode 100644 index 00000000..7fdb27b4 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/testing/FakeSigningConfigurationProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.testing; + +import com.android.tools.build.bundletool.model.ApksigSigningConfiguration; +import com.android.tools.build.bundletool.model.SigningConfigurationProvider; +import java.util.function.Function; + +/** Fake customizable implementation for {@link SigningConfigurationProvider}. */ +public class FakeSigningConfigurationProvider implements SigningConfigurationProvider { + + private final Function provider; + private boolean restrictedV3SigningConfig; + + public FakeSigningConfigurationProvider( + Function provider) { + this.provider = provider; + } + + public FakeSigningConfigurationProvider( + Function provider, + boolean restrictedV3SigningConfig) { + this.provider = provider; + this.restrictedV3SigningConfig = restrictedV3SigningConfig; + } + + @Override + public ApksigSigningConfiguration getSigningConfiguration(ApkDescription apkDescription) { + return provider.apply(apkDescription); + } + + @Override + public boolean hasRestrictedV3SigningConfig() { + return restrictedV3SigningConfig; + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java index 1bbe313b..bc00798e 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ManifestProtoUtils.java @@ -1051,12 +1051,12 @@ public static ManifestMutator withUsesSdkLibraryElement( .setValueAsString(certDigest)); } - /** Adds a element to an SDK Bundle manifest. */ - public static ManifestMutator withSdkPatchVersionProperty(int patchVersion) { + /** Adds a element to an SDK Bundle manifest. */ + public static ManifestMutator withSdkPatchVersionMetadata(int patchVersion) { return manifestElement -> manifestElement .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) - .getOrCreateChildElement(PROPERTY_ELEMENT_NAME) + .getOrCreateChildElement(META_DATA_ELEMENT_NAME) .addAttribute( createAndroidAttribute(NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) .setValueAsString(SDK_PATCH_VERSION_ATTRIBUTE_NAME)) @@ -1065,6 +1065,7 @@ public static ManifestMutator withSdkPatchVersionProperty(int patchVersion) { .setValueAsDecimalInteger(patchVersion)); } + /** Adds a {@value #PERMISSION_ELEMENT_NAME} element to the manifest. */ public static ManifestMutator withPermission() { return manifestElement -> diff --git a/src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java b/src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java index b3cc33c5..e7f08323 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/SdkBundleBuilder.java @@ -25,9 +25,12 @@ import com.android.tools.build.bundletool.model.BundleMetadata; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.version.BundleToolVersion; +import com.google.common.collect.ImmutableList; import com.google.common.io.ByteSource; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.nio.file.Path; import java.util.Optional; import java.util.zip.ZipFile; @@ -49,6 +52,8 @@ public class SdkBundleBuilder { private static final String SDK_PROVIDER_CLASS_NAME = "com.example.sandboxservice.MyAdsSdkEntryPoint"; + private static final String DEFAULT_MODULE_NAME = "base"; + private static final BundleMetadata METADATA = BundleMetadata.builder().build(); private BundleModule module = defaultModule(); @@ -62,7 +67,22 @@ public class SdkBundleBuilder { private Optional sdkInterfaceDescriptors = Optional.empty(); private static BundleModule defaultModule() { - return new BundleModuleBuilder("base").setManifest(createSdkAndroidManifest()).build(); + return new BundleModuleBuilder(DEFAULT_MODULE_NAME) + .setManifest(createSdkAndroidManifest()) + .build(); + } + + private static BundleModule defaultModuleWithAssets( + Path sdkBundlePath, ImmutableList assets) { + BundleModuleBuilder builder = new BundleModuleBuilder(DEFAULT_MODULE_NAME); + assets.forEach( + asset -> + builder.addFile( + asset.getAssetName(), + sdkBundlePath, + ZipPath.create(Path.of(DEFAULT_MODULE_NAME, asset.getAssetName()).toString()), + asset.getAssetContent())); + return builder.setManifest(createSdkAndroidManifest()).build(); } @CanIgnoreReturnValue @@ -109,6 +129,18 @@ public static RuntimeEnabledSdkVersion.Builder sdkVersionBuilder() { return RuntimeEnabledSdkVersion.newBuilder().setMajor(1).setMinor(1).setPatch(1); } + public SdkBundle buildWithAssets(Path sdkBundlePath, ImmutableList assets) { + SdkBundle.Builder sdkBundle = + SdkBundle.builder() + .setModule(defaultModuleWithAssets(sdkBundlePath, assets)) + .setSdkModulesConfig(sdkModulesConfig) + .setSdkBundleConfig(sdkBundleConfig) + .setBundleMetadata(METADATA) + .setVersionCode(versionCode); + sdkInterfaceDescriptors.ifPresent(sdkBundle::setSdkInterfaceDescriptors); + return sdkBundle.build(); + } + public SdkBundle build() { SdkBundle.Builder sdkBundle = SdkBundle.builder() diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java index 8db8db14..bb5a9632 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java @@ -23,6 +23,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallLocation; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.DEFAULT_SDK_MODULES_CONFIG; +import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.PACKAGE_NAME; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -64,7 +65,6 @@ public final class TestUtils { private static final byte[] TEST_CONTENT = new byte[1]; - private static final String PACKAGE_NAME = "com.test.sdk.detail"; private static final XmlNode MANIFEST = createSdkAndroidManifest(); public static final SdkMetadata DEFAULT_SDK_METADATA = getSdkMetadata(); @@ -238,6 +238,12 @@ public static ZipBuilder createZipBuilderForModules() { ZipPath.create("SdkModulesConfig.pb"), DEFAULT_SDK_MODULES_CONFIG.toByteArray()); } + public static ZipBuilder createZipBuilderForModulesWithoutConfig() { + return new ZipBuilder() + .addFileWithProtoContent(ZipPath.create("base/manifest/AndroidManifest.xml"), MANIFEST) + .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT); + } + public static ZipBuilder createZipBuilderForModulesWithoutManifest() { return new ZipBuilder() .addFileWithContent(ZipPath.create("base/dex/classes.dex"), TEST_CONTENT) diff --git a/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java index efa20d60..820487b2 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/CountrySetParityValidatorTest.java @@ -336,4 +336,34 @@ public void validateBundle_modulesWithAndWithoutCountrySet_succeeds() { new CountrySetParityValidator().validateBundle(appBundle); } + + @Test + public void validateBundle_modulesWithNestedTargeting_succeeds() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/img1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/img1#countries_latam/image.jpg") + .addFile("assets/img1#tcf_astc/image.jpg") + .addFile("assets/img1#tcf_pvrtc/image.jpg") + .addFile("assets/img1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + AppBundle appBundle = + AppBundle.buildFromModules( + /* modules= */ ImmutableList.of(moduleA), + /* bundleConfig= */ BundleConfig.newBuilder() + .setOptimizations( + Optimizations.newBuilder() + .setSplitsConfig( + SplitsConfig.newBuilder() + .addSplitDimension( + SplitDimension.newBuilder().setValue(Value.COUNTRY_SET)) + .addSplitDimension( + SplitDimension.newBuilder() + .setValue(Value.TEXTURE_COMPRESSION_FORMAT)))) + .build(), + /* bundleMetadata= */ BundleMetadata.builder().build()); + new CountrySetParityValidator().validateBundle(appBundle); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java index f4b77c52..a8498f52 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/DeviceTierParityValidatorTest.java @@ -188,4 +188,20 @@ public void tierNumbersNotContiguous_throws() { "All modules with device tier targeting must support the same contiguous" + " range of tier values starting from 0, but module 'a' supports [0, 2]."); } + + @Test + public void validateAllModules_withNestedTargeting_succeeds() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#countries_latam#tier_1/image.jpg") + .addFile("assets/img1#countries_latam#tier_0/image.jpg") + .addFile("assets/img1#countries_latam/image.jpg") + .addFile("assets/img1#tier_1/image.jpg") + .addFile("assets/img1#tier_0/image.jpg") + .addFile("assets/img1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + new DeviceTierParityValidator().validateAllModules(ImmutableList.of(moduleA)); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/NestedTargetingValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/NestedTargetingValidatorTest.java new file mode 100644 index 00000000..2fd144ce --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/NestedTargetingValidatorTest.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.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.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 NestedTargetingValidatorTest { + + @Test + public void validateAllModules_noTargeting_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 NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void validateAllModules_noNestedTargeting_ok() { + 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").setManifest(androidManifest("com.test.app")).build(); + + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void validateAllModules_multipleDirectoriesWithSameTargeting_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture#tcf_astc#countries_latam/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "Found multiple directories using same targeting values in module 'a'. Directory" + + " 'assets/texture' is targeted on following dimension values some of which vary" + + " only in nesting order: '[#countries_latam#tcf_astc," + + " #tcf_astc#countries_latam]'."); + } + + @Test + public void validateAllModules_differentNestedTargetingOnDifferentFolders_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1/image.jpg") + .addFile("assets/texture2#countries_sea#tcf_astc/image.jpg") + .addFile("assets/texture2#countries_sea/image.jpg") + .addFile("assets/texture2/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "Found directories targeted on different set of targeting dimensions in module 'a'." + + " Please make sure all directories are targeted on same set of targeting" + + " dimensions in same order."); + } + + @Test + public void validateAllModules_directoryTargetedOnDifferentSetOfTargetingDimension_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tier_1/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "Found directory targeted on different set of targeting dimensions in module 'a'." + + " Targeting Used: '[, #countries_latam#tcf_astc, #countries_latam#tier_1]'." + + " Please make sure all directories are targeted on same set of targeting" + + " dimensions in same order."); + } + + @Test + public void validateAllModules_directoryTargetedOnDifferentOrderOfTargetingDimension_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#tcf_pvrtc#countries_latam/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "Found directory targeted on different order of targeting dimensions in module 'a'." + + " Targeting Used: '[, #countries_latam, #countries_latam#tcf_astc," + + " #tcf_pvrtc#countries_latam]'. Please make sure all directories are targeted on" + + " same set of targeting dimensions in same order."); + } + + @Test + public void validateAllModules_folderNotTargetingAllCartesianProductOfDimensionValues_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_sea#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_sea#tcf_pvrtc/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA))); + + assertThat(exception) + .hasMessageThat() + .contains( + "Module 'a' uses nested targeting but does not define targeting for all of the points" + + " in the cartesian product of dimension values used."); + } + + @Test + public void validateAllModules_validNestedTargeting_succeeds() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1#countries_sea#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_sea#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_sea/image.jpg") + .addFile("assets/texture1#tcf_astc/image.jpg") + .addFile("assets/texture1#tcf_pvrtc/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + new NestedTargetingValidator().validateAllModules(/* modules= */ ImmutableList.of(moduleA)); + } + + @Test + public void validateAllModules_differentModulesWithDifferentNestedTargeting_throws() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1#countries_sea#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_sea#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_sea/image.jpg") + .addFile("assets/texture1#tcf_astc/image.jpg") + .addFile("assets/texture1#tcf_pvrtc/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1#tcf_astc/image.jpg") + .addFile("assets/texture1#tcf_pvrtc/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA, moduleB))); + + assertThat(exception) + .hasMessageThat() + .contains( + "Found different nested targeting across different modules. Please make sure all" + + " modules use same nested targeting."); + } + + @Test + public void validateAllModules_differentModulesWithSameNestedTargeting_succeeds() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1#tcf_astc/image.jpg") + .addFile("assets/texture1#tcf_pvrtc/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1#tcf_astc/image.jpg") + .addFile("assets/texture1#tcf_pvrtc/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA, moduleB)); + } + + @Test + public void validateAllModules_modulesWithAndWithoutNestedTargeting_succeeds() { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + BundleModule moduleB = + new BundleModuleBuilder("b") + .addFile("assets/texture1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/texture1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/texture1#countries_latam/image.jpg") + .addFile("assets/texture1#tcf_astc/image.jpg") + .addFile("assets/texture1#tcf_pvrtc/image.jpg") + .addFile("assets/texture1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + new NestedTargetingValidator() + .validateAllModules(/* modules= */ ImmutableList.of(moduleA, moduleB)); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidatorTest.java index d0838a17..f5039cb3 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkConfigValidatorTest.java @@ -15,6 +15,7 @@ */ package com.android.tools.build.bundletool.validation; +import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.SDK_PATCH_VERSION_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MAJOR_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MINOR_MAX_VALUE; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; @@ -333,6 +334,44 @@ public void validateAllModules_negativeVersionPatchInRuntimeEnabledSdkConfig_thr + " version."); } + @Test + public void validateAllModules_patchVersionTooBigInRuntimeEnabledSdkConfig_throws() { + BundleModule module = + new BundleModuleBuilder("module") + .setManifest(androidManifest("com.test.app")) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("package.name.1") + .setVersionMajor(0) + .setVersionMinor(1) + .setBuildTimeVersionPatch(SDK_PATCH_VERSION_MAX_VALUE + 1) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("package.name.2") + .setVersionMajor(1234) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(3)) + .build()) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new RuntimeEnabledSdkConfigValidator() + .validateAllModules(ImmutableList.of(module))); + assertThat(exception) + .hasMessageThat() + .contains( + "Found dependency on runtime-enabled SDK 'package.name.1' with illegal patch" + + " version. Patch version must be <= " + + SDK_PATCH_VERSION_MAX_VALUE); + } + @Test public void validateAllModules_versionMinorTooBigInRuntimeEnabledSdkConfig_throws() { BundleModule module = diff --git a/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java index 90e8761b..3a25a0f8 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java @@ -17,10 +17,10 @@ package com.android.tools.build.bundletool.validation; import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_LOCATION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.META_DATA_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_GROUP_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_TREE_ELEMENT_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.PROPERTY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_LIBRARY_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SHARED_USER_ID_ATTRIBUTE_NAME; @@ -31,7 +31,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withPermissionGroup; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withPermissionTree; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSdkLibraryElement; -import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSdkPatchVersionProperty; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSdkPatchVersionMetadata; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSharedUserId; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitNameService; @@ -77,7 +77,7 @@ public void manifest_withSdkPatchVersionProperty_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) .setManifest( - androidManifest(PKG_NAME, withSdkPatchVersionProperty(/* patchVersion= */ 14))) + androidManifest(PKG_NAME, withSdkPatchVersionMetadata(/* patchVersion= */ 14))) .build(); Throwable exception = @@ -88,7 +88,7 @@ public void manifest_withSdkPatchVersionProperty_throws() { .hasMessageThat() .isEqualTo( "<" - + PROPERTY_ELEMENT_NAME + + META_DATA_ELEMENT_NAME + "> cannot be declared with name='" + SDK_PATCH_VERSION_ATTRIBUTE_NAME + "' in the manifest of an SDK bundle."); diff --git a/src/test/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidatorTest.java index 52f143d8..27d02b13 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/SdkModulesConfigValidatorTest.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.validation; +import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.SDK_PATCH_VERSION_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MAJOR_MAX_VALUE; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MINOR_MAX_VALUE; import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; @@ -88,7 +89,16 @@ public void minorVersionNegative_throws() throws IOException { public void patchVersionNegative_throws() throws IOException { assertExceptionThrown( createSdkModulesConfig().setSdkVersion(RuntimeEnabledSdkVersion.newBuilder().setPatch(-3)), - "SDK patch version must be a non-negative integer"); + "SDK patch version must be an integer between 0 and " + SDK_PATCH_VERSION_MAX_VALUE); + } + + @Test + public void patchVersionTooBig_throws() throws IOException { + assertExceptionThrown( + createSdkModulesConfig() + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder().setPatch(SDK_PATCH_VERSION_MAX_VALUE + 1)), + "SDK patch version must be an integer between 0 and " + SDK_PATCH_VERSION_MAX_VALUE); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/validation/StandaloneFeatureModuleValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/StandaloneFeatureModuleValidatorTest.java new file mode 100644 index 00000000..bdfa8023 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/StandaloneFeatureModuleValidatorTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.validation; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifestForAssetModule; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifestForFeature; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkCondition; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withOnDemandDelivery; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.bundle.Config.StandaloneConfig.FeatureModulesMode; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; +import com.android.tools.build.bundletool.testing.BundleConfigBuilder; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class StandaloneFeatureModuleValidatorTest { + + private final StandaloneFeatureModulesValidator validator = + new StandaloneFeatureModulesValidator(); + + @Test + public void fusedFeatureModules_minSdkHigherThan21_ok() { + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.FUSED_FEATURE_MODULES) + .build()) + .addModule( + new BundleModuleBuilder(BundleModuleName.BASE_MODULE_NAME.getName()) + .setManifest(androidManifest("com.app", withMinSdkVersion(22))) + .build()) + .build(); + + validator.validateBundle(appBundle); + } + + @Test + public void assetPackOnlyBundle_ok() { + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.FUSED_FEATURE_MODULES) + .build()) + .addModule( + new BundleModuleBuilder("asset_pack") + .setManifest(androidManifestForAssetModule("com.app")) + .build()) + .build(); + + validator.validateBundle(appBundle); + } + + @Test + public void standaloneFeatureModules_multipleOnDemandFeatureModules_ok() { + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.SEPARATE_FEATURE_MODULES) + .build()) + .addModule( + new BundleModuleBuilder(BundleModuleName.BASE_MODULE_NAME.getName()) + .setManifest(androidManifest("com.app", withMinSdkVersion(19))) + .build()) + .addModule( + new BundleModuleBuilder("feature1") + .setManifest(androidManifestForFeature("com.app", withOnDemandDelivery())) + .build()) + .addModule( + new BundleModuleBuilder("feature2") + .setManifest(androidManifestForFeature("com.app", withOnDemandDelivery())) + .build()) + .build(); + + validator.validateBundle(appBundle); + } + + @Test + public void standaloneFeatureModules_minSdkHigherThan21_throws() { + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.SEPARATE_FEATURE_MODULES) + .build()) + .addModule( + new BundleModuleBuilder(BundleModuleName.BASE_MODULE_NAME.getName()) + .setManifest(androidManifest("com.app", withMinSdkVersion(22))) + .build()) + .build(); + + InvalidBundleException exception = + assertThrows(InvalidBundleException.class, () -> validator.validateBundle(appBundle)); + assertThat(exception) + .hasMessageThat() + .contains( + "STANDALONE_FEATURE_MODULES can only be used for Android App Bundles with minSdk <" + + " 21."); + } + + @Test + public void standaloneFeatureModules_assetModule_throws() { + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.SEPARATE_FEATURE_MODULES) + .build()) + .addModule( + new BundleModuleBuilder(BundleModuleName.BASE_MODULE_NAME.getName()) + .setManifest(androidManifest("com.app", withMinSdkVersion(19))) + .build()) + .addModule( + new BundleModuleBuilder("asset_pack") + .setManifest(androidManifestForAssetModule("com.app")) + .build()) + .build(); + + InvalidBundleException exception = + assertThrows(InvalidBundleException.class, () -> validator.validateBundle(appBundle)); + assertThat(exception) + .hasMessageThat() + .contains( + "Asset modules are not supported for Android App Bundles with" + + " STANDALONE_FEATURE_MODULES enabled."); + } + + @Test + public void standaloneFeatureModules_conditionalFeatureModule_throws() { + AppBundle appBundle = + new AppBundleBuilder() + .setBundleConfig( + BundleConfigBuilder.create() + .setFeatureModulesModeForStandalone(FeatureModulesMode.SEPARATE_FEATURE_MODULES) + .build()) + .addModule( + new BundleModuleBuilder(BundleModuleName.BASE_MODULE_NAME.getName()) + .setManifest(androidManifest("com.app", withMinSdkVersion(19))) + .build()) + .addModule( + new BundleModuleBuilder("feature") + .setManifest(androidManifestForFeature("com.app", withMinSdkCondition(23))) + .build()) + .build(); + + InvalidBundleException exception = + assertThrows(InvalidBundleException.class, () -> validator.validateBundle(appBundle)); + assertThat(exception) + .hasMessageThat() + .contains( + "Only on-demand feature modules are supported for Android App Bundles with " + + "STANDALONE_FEATURE_MODULES enabled."); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidatorTest.java index e7dce262..7dff62f1 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/TextureCompressionFormatParityValidatorTest.java @@ -313,4 +313,20 @@ public void differentFallbacks_throws() throws Exception { + " module 'a' has formats [ASTC] (with fallback directories) and module 'b' has" + " formats [ASTC] (without fallback directories)."); } + + @Test + public void validateAllModules_withNestedTargeting_succeeds() throws Exception { + BundleModule moduleA = + new BundleModuleBuilder("a") + .addFile("assets/img1#countries_latam#tcf_astc/image.jpg") + .addFile("assets/img1#countries_latam#tcf_pvrtc/image.jpg") + .addFile("assets/img1#countries_latam/image.jpg") + .addFile("assets/img1#tcf_astc/image.jpg") + .addFile("assets/img1#tcf_pvrtc/image.jpg") + .addFile("assets/img1/image.jpg") + .setManifest(androidManifest("com.test.app")) + .build(); + + new TextureCompressionFormatParityValidator().validateAllModules(ImmutableList.of(moduleA)); + } } diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tcf_assets_config.textpb b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tcf_assets_config.textpb new file mode 100644 index 00000000..8f9cd1c1 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tcf_assets_config.textpb @@ -0,0 +1,153 @@ +directory { + path: "assets/textures#countries_latam#tcf_astc" + targeting { + texture_compression_format { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set { + value: "latam" + alternatives: "sea" + } + } +} +directory { + path: "assets/textures#countries_latam#tcf_pvrtc" + targeting { + texture_compression_format { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set { + value: "latam" + alternatives: "sea" + } + } +} +directory { + path: "assets/textures#countries_latam" + targeting { + texture_compression_format { + alternatives { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set { + value: "latam" + alternatives: "sea" + } + } +} +directory { + path: "assets/textures#countries_sea#tcf_astc" + targeting { + texture_compression_format { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set { + value: "sea" + alternatives: "latam" + } + } +} +directory { + path: "assets/textures#countries_sea#tcf_pvrtc" + targeting { + texture_compression_format { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set { + value: "sea" + alternatives: "latam" + } + } +} +directory { + path: "assets/textures#countries_sea" + targeting { + texture_compression_format { + alternatives { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set { + value: "sea" + alternatives: "latam" + } + } +} +directory { + path: "assets/textures#tcf_astc" + targeting { + texture_compression_format { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set { + alternatives: "latam" + alternatives: "sea" + } + } +} +directory { + path: "assets/textures#tcf_pvrtc" + targeting { + texture_compression_format { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set { + alternatives: "latam" + alternatives: "sea" + } + } +} +directory { + path: "assets/textures" + targeting { + texture_compression_format { + alternatives { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set { + alternatives: "latam" + alternatives: "sea" + } + } +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tcf_toc.textpb b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tcf_toc.textpb new file mode 100644 index 00000000..3df76e2b --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tcf_toc.textpb @@ -0,0 +1,370 @@ +variant { + targeting { + sdk_version_targeting { + value { + min { + value: 1 + } + } + alternatives { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + } + } + apk_set { + module_metadata { + name: "base" + targeting { + } + delivery_type: INSTALL_TIME + module_type: FEATURE_MODULE + } + apk_description { + targeting { + texture_compression_format_targeting { + value { + alias: ASTC + } + } + country_set_targeting { + value: "latam" + } + } + path: "standalones/standalone-astc_countries_latam.apk" + standalone_apk_metadata { + fused_module_name: "assetpack1" + fused_module_name: "base" + } + } + } + variant_properties { + } +} +variant { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + alternatives { + min { + value: 1 + } + } + } + } + apk_set { + module_metadata { + name: "base" + targeting { + } + delivery_type: INSTALL_TIME + module_type: FEATURE_MODULE + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + } + path: "splits/base-master.apk" + split_apk_metadata { + is_master_split: true + } + } + } + variant_number: 1 + variant_properties { + } +} +asset_slice_set { + asset_module_metadata { + name: "assetpack1" + instant_metadata { + } + delivery_type: INSTALL_TIME + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set_targeting { + value: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-astc_countries_latam.apk" + asset_slice_metadata { + split_id: "assetpack1.config.astc_countries_latam" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set_targeting { + value: "sea" + alternatives: "latam" + } + } + path: "asset-slices/assetpack1-astc_countries_sea.apk" + asset_slice_metadata { + split_id: "assetpack1.config.astc_countries_sea" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + country_set_targeting { + alternatives: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-astc_other_countries.apk" + asset_slice_metadata { + split_id: "assetpack1.config.astc_other_countries" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set_targeting { + value: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-pvrtc_countries_latam.apk" + asset_slice_metadata { + split_id: "assetpack1.config.pvrtc_countries_latam" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set_targeting { + value: "sea" + alternatives: "latam" + } + } + path: "asset-slices/assetpack1-pvrtc_countries_sea.apk" + asset_slice_metadata { + split_id: "assetpack1.config.pvrtc_countries_sea" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set_targeting { + alternatives: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-pvrtc_other_countries.apk" + asset_slice_metadata { + split_id: "assetpack1.config.pvrtc_other_countries" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + alternatives { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set_targeting { + value: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-other_tcf_countries_latam.apk" + asset_slice_metadata { + split_id: "assetpack1.config.other_tcf_countries_latam" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + alternatives { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set_targeting { + value: "sea" + alternatives: "latam" + } + } + path: "asset-slices/assetpack1-other_tcf_countries_sea.apk" + asset_slice_metadata { + split_id: "assetpack1.config.other_tcf_countries_sea" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + alternatives { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + country_set_targeting { + alternatives: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-other_tcf_other_countries.apk" + asset_slice_metadata { + split_id: "assetpack1.config.other_tcf_other_countries" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + } + path: "asset-slices/assetpack1-master.apk" + asset_slice_metadata { + split_id: "assetpack1" + is_master_split: true + } + } +} +package_name: "com.test.app" +local_testing_info { +} +default_targeting_value { + dimension: COUNTRY_SET + default_value: "latam" +} +default_targeting_value { + dimension: TEXTURE_COMPRESSION_FORMAT + default_value: "astc" +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tier_assets_config.textpb b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tier_assets_config.textpb new file mode 100644 index 00000000..311db6ec --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tier_assets_config.textpb @@ -0,0 +1,168 @@ +directory: { + path: "assets/textures#countries_latam#tier_0" + targeting: { + device_tier: { + value: { + } + alternatives: { + value: 1 + } + alternatives: { + value: 2 + } + } + country_set: { + value: ["latam"] + alternatives: ["sea"] + } + } +} +directory: { + path: "assets/textures#countries_latam#tier_1" + targeting: { + device_tier: { + value: { + value: 1 + } + alternatives: { + value: 2 + } + alternatives: { + } + } + country_set: { + value: ["latam"] + alternatives: ["sea"] + } + } +} +directory: { + path: "assets/textures#countries_latam#tier_2" + targeting: { + device_tier: { + value: { + value: 2 + } + alternatives: { + value: 1 + } + alternatives: { + } + } + country_set: { + value: ["latam"] + alternatives: ["sea"] + } + } +} +directory: { + path: "assets/textures#countries_sea#tier_0" + targeting: { + device_tier: { + value: { + } + alternatives: { + value: 1 + } + alternatives: { + value: 2 + } + } + country_set: { + value: ["sea"] + alternatives: ["latam"] + } + } +} +directory: { + path: "assets/textures#countries_sea#tier_1" + targeting: { + device_tier: { + value: { + value: 1 + } + alternatives: { + value: 2 + } + alternatives: { + } + } + country_set: { + value: ["sea"] + alternatives: ["latam"] + } + } +} +directory: { + path: "assets/textures#countries_sea#tier_2" + targeting: { + device_tier: { + value: { + value: 2 + } + alternatives: { + value: 1 + } + alternatives: { + } + } + country_set: { + value: ["sea"] + alternatives: ["latam"] + } + } +} +directory: { + path: "assets/textures#tier_0" + targeting: { + device_tier: { + value: { + } + alternatives: { + value: 1 + } + alternatives: { + value: 2 + } + } + country_set: { + alternatives: ["latam", "sea"] + } + } +} +directory: { + path: "assets/textures#tier_1" + targeting: { + device_tier: { + value: { + value: 1 + } + alternatives: { + value: 2 + } + alternatives: { + } + } + country_set: { + alternatives: ["latam", "sea"] + } + } +} +directory: { + path: "assets/textures#tier_2" + targeting: { + device_tier: { + value: { + value: 2 + } + alternatives: { + value: 1 + } + alternatives: { + } + } + country_set: { + alternatives: ["latam", "sea"] + } + } +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tier_toc.textpb b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tier_toc.textpb new file mode 100644 index 00000000..3fe73883 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/country_tier_toc.textpb @@ -0,0 +1,382 @@ +variant { + targeting { + sdk_version_targeting { + value { + min { + value: 1 + } + } + alternatives { + min { + value: 21 + } + } + } + } + apk_set { + module_metadata { + name: "base" + targeting { + } + delivery_type: INSTALL_TIME + module_type: FEATURE_MODULE + } + apk_description { + targeting { + device_tier_targeting { + value { + } + } + country_set_targeting { + value: "latam" + } + } + path: "standalones/standalone-tier_0_countries_latam.apk" + standalone_apk_metadata { + fused_module_name: "assetpack1" + fused_module_name: "base" + } + } + } + variant_properties { + } +} +variant { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + alternatives { + min { + value: 1 + } + } + } + } + apk_set { + module_metadata { + name: "base" + targeting { + } + delivery_type: INSTALL_TIME + module_type: FEATURE_MODULE + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + } + path: "splits/base-master.apk" + split_apk_metadata { + is_master_split: true + } + } + } + variant_number: 1 + variant_properties { + } +} +asset_slice_set { + asset_module_metadata { + name: "assetpack1" + instant_metadata { + } + delivery_type: INSTALL_TIME + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + } + alternatives { + value: 1 + } + alternatives { + value: 2 + } + } + country_set_targeting { + value: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-tier_0_countries_latam.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_0_countries_latam" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + } + alternatives { + value: 1 + } + alternatives { + value: 2 + } + } + country_set_targeting { + value: "sea" + alternatives: "latam" + } + } + path: "asset-slices/assetpack1-tier_0_countries_sea.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_0_countries_sea" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + } + alternatives { + value: 1 + } + alternatives { + value: 2 + } + } + country_set_targeting { + alternatives: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-tier_0_other_countries.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_0_other_countries" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + value: 1 + } + alternatives { + } + alternatives { + value: 2 + } + } + country_set_targeting { + value: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-tier_1_countries_latam.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_1_countries_latam" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + value: 1 + } + alternatives { + } + alternatives { + value: 2 + } + } + country_set_targeting { + value: "sea" + alternatives: "latam" + } + } + path: "asset-slices/assetpack1-tier_1_countries_sea.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_1_countries_sea" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + value: 1 + } + alternatives { + } + alternatives { + value: 2 + } + } + country_set_targeting { + alternatives: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-tier_1_other_countries.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_1_other_countries" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + value: 2 + } + alternatives { + } + alternatives { + value: 1 + } + } + country_set_targeting { + value: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-tier_2_countries_latam.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_2_countries_latam" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + value: 2 + } + alternatives { + } + alternatives { + value: 1 + } + } + country_set_targeting { + value: "sea" + alternatives: "latam" + } + } + path: "asset-slices/assetpack1-tier_2_countries_sea.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_2_countries_sea" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + device_tier_targeting { + value { + value: 2 + } + alternatives { + } + alternatives { + value: 1 + } + } + country_set_targeting { + alternatives: "latam" + alternatives: "sea" + } + } + path: "asset-slices/assetpack1-tier_2_other_countries.apk" + asset_slice_metadata { + split_id: "assetpack1.config.tier_2_other_countries" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + } + path: "asset-slices/assetpack1-master.apk" + asset_slice_metadata { + split_id: "assetpack1" + is_master_split: true + } + } +} +package_name: "com.test.app" +local_testing_info { +} +default_targeting_value { + dimension: COUNTRY_SET + default_value: "latam" +} +default_targeting_value { + dimension: DEVICE_TIER + default_value: "0" +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/tier_tcf_assets_config.textpb b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/tier_tcf_assets_config.textpb new file mode 100644 index 00000000..f49d9438 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/tier_tcf_assets_config.textpb @@ -0,0 +1,207 @@ +directory: { + path: "assets/textures#tier_0#tcf_astc" + targeting: { + texture_compression_format: { + value: { + alias: ASTC + } + alternatives: { + alias: PVRTC + } + } + device_tier: { + value: { + } + alternatives: { + value: 1 + } + alternatives: { + value: 2 + } + } + } +} +directory: { + path: "assets/textures#tier_0#tcf_pvrtc" + targeting: { + texture_compression_format: { + value: { + alias: PVRTC + } + alternatives: { + alias: ASTC + } + } + device_tier: { + value: { + } + alternatives: { + value: 1 + } + alternatives: { + value: 2 + } + } + } +} +directory: { + path: "assets/textures#tier_0" + targeting: { + texture_compression_format: { + alternatives: { + alias: ASTC + } + alternatives: { + alias: PVRTC + } + } + device_tier: { + value: { + } + alternatives: { + value: 1 + } + alternatives: { + value: 2 + } + } + } +} +directory: { + path: "assets/textures#tier_1#tcf_astc" + targeting: { + texture_compression_format: { + value: { + alias: ASTC + } + alternatives: { + alias: PVRTC + } + } + device_tier: { + value: { + value: 1 + } + alternatives: { + value: 2 + } + alternatives: { + } + } + } +} +directory: { + path: "assets/textures#tier_1#tcf_pvrtc" + targeting: { + texture_compression_format: { + value: { + alias: PVRTC + } + alternatives: { + alias: ASTC + } + } + device_tier: { + value: { + value: 1 + } + alternatives: { + value: 2 + } + alternatives: { + } + } + } +} +directory: { + path: "assets/textures#tier_1" + targeting: { + texture_compression_format: { + alternatives: { + alias: ASTC + } + alternatives: { + alias: PVRTC + } + } + device_tier: { + value: { + value: 1 + } + alternatives: { + value: 2 + } + alternatives: { + } + } + } +} +directory: { + path: "assets/textures#tier_2#tcf_astc" + targeting: { + texture_compression_format: { + value: { + alias: ASTC + } + alternatives: { + alias: PVRTC + } + } + device_tier: { + value: { + value: 2 + } + alternatives: { + value: 1 + } + alternatives: { + } + } + } +} +directory: { + path: "assets/textures#tier_2#tcf_pvrtc" + targeting: { + texture_compression_format: { + value: { + alias: PVRTC + } + alternatives: { + alias: ASTC + } + } + device_tier: { + value: { + value: 2 + } + alternatives: { + value: 1 + } + alternatives: { + } + } + } +} +directory: { + path: "assets/textures#tier_2" + targeting: { + texture_compression_format: { + alternatives: { + alias: ASTC + } + alternatives: { + alias: PVRTC + } + } + device_tier: { + value: { + value: 2 + } + alternatives: { + value: 1 + } + alternatives: { + } + } + } +} diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/tier_tcf_toc.textpb b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/tier_tcf_toc.textpb new file mode 100644 index 00000000..7cc55b29 --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/nested_targeting/tier_tcf_toc.textpb @@ -0,0 +1,425 @@ +variant { + targeting { + sdk_version_targeting { + value { + min { + value: 1 + } + } + alternatives { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + } + } + apk_set { + module_metadata { + name: "base" + targeting { + } + delivery_type: INSTALL_TIME + module_type: FEATURE_MODULE + } + apk_description { + targeting { + texture_compression_format_targeting { + value { + alias: ASTC + } + } + device_tier_targeting { + value { + } + } + } + path: "standalones/standalone-astc_tier_0.apk" + standalone_apk_metadata { + fused_module_name: "assetpack1" + fused_module_name: "base" + } + } + } + variant_properties { + } +} +variant { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + alternatives { + min { + value: 1 + } + } + } + } + apk_set { + module_metadata { + name: "base" + targeting { + } + delivery_type: INSTALL_TIME + module_type: FEATURE_MODULE + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + } + path: "splits/base-master.apk" + split_apk_metadata { + is_master_split: true + } + } + } + variant_number: 1 + variant_properties { + } +} +asset_slice_set { + asset_module_metadata { + name: "assetpack1" + instant_metadata { + } + delivery_type: INSTALL_TIME + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + device_tier_targeting { + value { + } + alternatives { + value: 1 + } + alternatives { + value: 2 + } + } + } + path: "asset-slices/assetpack1-astc_tier_0.apk" + asset_slice_metadata { + split_id: "assetpack1.config.astc_tier_0" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + device_tier_targeting { + value { + value: 1 + } + alternatives { + } + alternatives { + value: 2 + } + } + } + path: "asset-slices/assetpack1-astc_tier_1.apk" + asset_slice_metadata { + split_id: "assetpack1.config.astc_tier_1" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: ASTC + } + alternatives { + alias: PVRTC + } + } + device_tier_targeting { + value { + value: 2 + } + alternatives { + } + alternatives { + value: 1 + } + } + } + path: "asset-slices/assetpack1-astc_tier_2.apk" + asset_slice_metadata { + split_id: "assetpack1.config.astc_tier_2" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + device_tier_targeting { + value { + } + alternatives { + value: 1 + } + alternatives { + value: 2 + } + } + } + path: "asset-slices/assetpack1-pvrtc_tier_0.apk" + asset_slice_metadata { + split_id: "assetpack1.config.pvrtc_tier_0" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + device_tier_targeting { + value { + value: 1 + } + alternatives { + } + alternatives { + value: 2 + } + } + } + path: "asset-slices/assetpack1-pvrtc_tier_1.apk" + asset_slice_metadata { + split_id: "assetpack1.config.pvrtc_tier_1" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + value { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + device_tier_targeting { + value { + value: 2 + } + alternatives { + } + alternatives { + value: 1 + } + } + } + path: "asset-slices/assetpack1-pvrtc_tier_2.apk" + asset_slice_metadata { + split_id: "assetpack1.config.pvrtc_tier_2" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + alternatives { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + device_tier_targeting { + value { + } + alternatives { + value: 1 + } + alternatives { + value: 2 + } + } + } + path: "asset-slices/assetpack1-other_tcf_tier_0.apk" + asset_slice_metadata { + split_id: "assetpack1.config.other_tcf_tier_0" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + alternatives { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + device_tier_targeting { + value { + value: 1 + } + alternatives { + } + alternatives { + value: 2 + } + } + } + path: "asset-slices/assetpack1-other_tcf_tier_1.apk" + asset_slice_metadata { + split_id: "assetpack1.config.other_tcf_tier_1" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + texture_compression_format_targeting { + alternatives { + alias: PVRTC + } + alternatives { + alias: ASTC + } + } + device_tier_targeting { + value { + value: 2 + } + alternatives { + } + alternatives { + value: 1 + } + } + } + path: "asset-slices/assetpack1-other_tcf_tier_2.apk" + asset_slice_metadata { + split_id: "assetpack1.config.other_tcf_tier_2" + } + } + apk_description { + targeting { + sdk_version_targeting { + value { + min { + value: 21 + } + } + } + } + path: "asset-slices/assetpack1-master.apk" + asset_slice_metadata { + split_id: "assetpack1" + is_master_split: true + } + } +} +package_name: "com.test.app" +local_testing_info { +} +default_targeting_value { + dimension: DEVICE_TIER + default_value: "0" +} +default_targeting_value { + dimension: TEXTURE_COMPRESSION_FORMAT + default_value: "astc" +}