diff --git a/README.md b/README.md index 04d5322b..fcdb3632 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.15.1](https://github.com/google/bundletool/releases) +Latest release: [1.15.2](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index 2dfcd6c8..d1491fc1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.15.1 +release_version = 1.15.2 diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java index 61af5968..ad0be0ae 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildApksCommand.java @@ -631,11 +631,10 @@ public BuildApksCommand build() { .build(); case SYSTEM: DeviceSpec deviceSpec = command.getDeviceSpec().get(); - if (deviceSpec.getScreenDensity() == 0 || deviceSpec.getSupportedAbisList().isEmpty()) { + if (deviceSpec.getSupportedAbisList().isEmpty()) { throw InvalidCommandException.builder() .withInternalMessage( - "Device spec must have screen density and ABIs set when running with " - + "'%s' mode flag. ", + "Device spec must have ABIs set when running with '%s' mode flag.", SYSTEM.getLowerCaseName()) .build(); } 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 84484936..f4157a50 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 @@ -19,6 +19,7 @@ import static com.android.tools.build.bundletool.commands.BuildApksCommand.ApkBuildMode.SYSTEM; import static com.android.tools.build.bundletool.commands.ExtractApksCommand.ALL_MODULES_SHORTCUT; import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESOURCES_REFERENCED_IN_MANIFEST_TO_MASTER_SPLIT; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; @@ -43,6 +44,7 @@ import com.android.tools.build.bundletool.model.GeneratedAssetSlices; import com.android.tools.build.bundletool.model.ModuleDeliveryType; import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.OptimizationDimension; import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException; import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.targeting.AlternativeVariantTargetingPopulator; @@ -325,7 +327,7 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat apkGenerationConfiguration.setEnableUncompressedNativeLibraries( apkOptimizations.getUncompressNativeLibraries()); apkGenerationConfiguration.setEnableDexCompressionSplitter( - apkOptimizations.getUncompressDexFiles()); + getEnableUncompressedDexOptimization(appBundle)); apkGenerationConfiguration.setDexCompressionSplitterForTargetSdk( apkOptimizations.getUncompressedDexTargetSdk()); apkGenerationConfiguration.setEnableSparseEncodingVariant( @@ -362,6 +364,20 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat return apkGenerationConfiguration; } + private boolean getEnableUncompressedDexOptimization(AppBundle appBundle) { + if (appBundle.getUncompressedDexOptOut()) { + return false; + } + if (appBundle.getBundleConfig().getOptimizations().hasUncompressDexFiles()) { + // If uncompressed dex is specified in the BundleConfig it will be honoured. + return appBundle.getBundleConfig().getOptimizations().getUncompressDexFiles().getEnabled(); + } + // This is the default value of the optimisation. Depends on the bundletool version. + boolean enableUncompressedDexOptimization = apkOptimizations.getUncompressDexFiles(); + + return enableUncompressedDexOptimization; + } + private ApkGenerationConfiguration getAssetSliceGenerationConfiguration() { return ApkGenerationConfiguration.builder() .setEnableBaseModuleMinSdkAsDefaultTargeting( @@ -397,7 +413,18 @@ private boolean matchModuleToDevice(BundleModule module) { private ApkOptimizations getSystemApkOptimizations() { ImmutableSet systemApkOptions = command.getSystemApkOptions(); - return apkOptimizations.toBuilder() + ApkOptimizations.Builder apkOptimizationsBuilder = apkOptimizations.toBuilder(); + + checkArgument(deviceSpec.isPresent(), "Must specify a device spec in system mode"); + if (deviceSpec.get().getScreenDensity() == 0) { + // If no screen density is specified, then don't split by screen density. + apkOptimizationsBuilder.setSplitDimensions( + Sets.difference( + apkOptimizations.getSplitDimensions(), + ImmutableSet.of(OptimizationDimension.SCREEN_DENSITY)) + .immutableCopy()); + } + return apkOptimizationsBuilder .setUncompressNativeLibraries( systemApkOptions.contains(SystemApkOption.UNCOMPRESSED_NATIVE_LIBRARIES)) .setUncompressDexFiles(systemApkOptions.contains(SystemApkOption.UNCOMPRESSED_DEX_FILES)) diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommand.java index 66ba7e4a..15226e89 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommand.java @@ -16,15 +16,21 @@ package com.android.tools.build.bundletool.commands; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.OutputFormat.APK_SET; +import static com.android.tools.build.bundletool.commands.BuildApksCommand.OutputFormat.DIRECTORY; import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; import static com.android.tools.build.bundletool.model.utils.BundleParser.getModulesZip; +import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileHasExtension; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; import com.android.bundle.RuntimeEnabledSdkConfigProto.SdkSplitPropertiesInheritedFromApp; import com.android.tools.build.bundletool.androidtools.Aapt2Command; +import com.android.tools.build.bundletool.commands.BuildApksCommand.OutputFormat; import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; import com.android.tools.build.bundletool.flags.Flag; @@ -41,10 +47,12 @@ import com.android.tools.build.bundletool.model.utils.DefaultSystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.files.BufferedIo; +import com.android.tools.build.bundletool.model.utils.files.FileUtils; import com.android.tools.build.bundletool.sdkmodule.SdkModuleToAppBundleModuleConverter; import com.android.tools.build.bundletool.validation.SdkAsarValidator; import com.android.tools.build.bundletool.validation.SdkBundleValidator; import com.google.auto.value.AutoValue; +import com.google.common.io.MoreFiles; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -53,9 +61,12 @@ import java.io.PrintStream; import java.io.Reader; import java.io.UncheckedIOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executors; +import java.util.logging.Logger; import java.util.zip.ZipException; import java.util.zip.ZipFile; @@ -65,8 +76,12 @@ public abstract class BuildSdkApksForAppCommand { private static final int DEFAULT_THREAD_POOL_SIZE = 4; + private static final String APK_SET_ARCHIVE_EXTENSION = "apks"; + public static final String COMMAND_NAME = "build-sdk-apks-for-app"; + private static final Logger logger = Logger.getLogger(BuildSdkApksForAppCommand.class.getName()); + private static final Flag SDK_BUNDLE_LOCATION_FLAG = Flag.path("sdk-bundle"); private static final Flag SDK_ARCHIVE_LOCATION_FLAG = Flag.path("sdk-archive"); @@ -76,6 +91,9 @@ public abstract class BuildSdkApksForAppCommand { private static final Flag OUTPUT_FILE_FLAG = Flag.path("output"); + private static final Flag OUTPUT_FORMAT_FLAG = + Flag.enumFlag("output-format", OutputFormat.class); + private static final Flag AAPT2_PATH_FLAG = Flag.path("aapt2"); // Signing-related flags: should match flags from apksig library. @@ -95,6 +113,8 @@ public abstract class BuildSdkApksForAppCommand { public abstract Path getOutputFile(); + public abstract OutputFormat getOutputFormat(); + public abstract Optional getAapt2Command(); public abstract Optional getSigningConfiguration(); @@ -108,7 +128,7 @@ ListeningExecutorService getExecutorService() { abstract boolean isExecutorServiceCreatedByBundleTool(); public static BuildSdkApksForAppCommand.Builder builder() { - return new AutoValue_BuildSdkApksForAppCommand.Builder(); + return new AutoValue_BuildSdkApksForAppCommand.Builder().setOutputFormat(APK_SET); } /** Builder for {@link BuildSdkApksForAppCommand}. */ @@ -122,7 +142,7 @@ public abstract static class Builder { public abstract Builder setSdkArchivePath(Path sdkArchivePath); /** Sets the config containing app properties that the SDK split should inherit. */ - abstract Builder setInheritedAppProperties( + public abstract Builder setInheritedAppProperties( SdkSplitPropertiesInheritedFromApp sdkSplitPropertiesInheritedFromApp); /** Sets path to a config file containing app properties that the SDK split should inherit. */ @@ -133,6 +153,9 @@ public Builder setInheritedAppProperties(Path inheritedAppProperties) { /** Path to the output produced by this command. Must have extension ".apks". */ public abstract Builder setOutputFile(Path outputFile); + /** Sets the output format. */ + public abstract Builder setOutputFormat(OutputFormat outputFormat); + /** Provides a wrapper around the execution of the aapt2 command. */ public abstract Builder setAapt2Command(Aapt2Command aapt2Command); @@ -211,7 +234,22 @@ public static CommandHelp help() { FlagDescription.builder() .setFlagName(OUTPUT_FILE_FLAG.getName()) .setExampleValue("output.apks") - .setDescription("Path to where the APK Set archive should be created.") + .setDescription( + "Path to where the APK Set archive should be created (default) or path to the" + + " directory where generated APKs should be stored when flag --%s is set" + + " to '%s'.", + OUTPUT_FORMAT_FLAG.getName(), DIRECTORY) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName(OUTPUT_FORMAT_FLAG.getName()) + .setExampleValue(joinFlagOptions(OutputFormat.values())) + .setOptional(true) + .setDescription( + "Specifies output format for generated APKs. If set to '%s' outputs APKs into" + + " the created APK Set archive (default). If set to '%s' outputs APKs" + + " into the specified directory.", + APK_SET, DIRECTORY) .build()) .addFlag( FlagDescription.builder() @@ -267,6 +305,10 @@ public static CommandHelp help() { .build(); } + private static String joinFlagOptions(Enum... flagOptions) { + return stream(flagOptions).map(Enum::name).map(String::toLowerCase).collect(joining("|")); + } + public static BuildSdkApksForAppCommand fromFlags(ParsedFlags flags) { return fromFlags(flags, System.out, DEFAULT_PROVIDER); } @@ -278,6 +320,7 @@ static BuildSdkApksForAppCommand fromFlags( .setInheritedAppProperties( INHERITED_APP_PROPERTIES_LOCATION_FLAG.getRequiredValue(flags)) .setOutputFile(OUTPUT_FILE_FLAG.getRequiredValue(flags)); + OUTPUT_FORMAT_FLAG.getValue(flags).ifPresent(command::setOutputFormat); SDK_BUNDLE_LOCATION_FLAG.getValue(flags).ifPresent(command::setSdkBundlePath); SDK_ARCHIVE_LOCATION_FLAG.getValue(flags).ifPresent(command::setSdkArchivePath); AAPT2_PATH_FLAG @@ -290,15 +333,26 @@ static BuildSdkApksForAppCommand fromFlags( return command.build(); } - public void execute() { + @CanIgnoreReturnValue + public Path execute() { validateInput(); + + Path outputDirectory = + getOutputFormat().equals(APK_SET) ? getOutputFile().getParent() : getOutputFile(); + if (outputDirectory != null && Files.notExists(outputDirectory)) { + logger.info("Output directory '" + outputDirectory + "' does not exist, creating it."); + FileUtils.createDirectories(outputDirectory); + } + if (getSdkBundlePath().isPresent()) { executeForSdkBundle(); } else if (getSdkArchivePath().isPresent()) { executeForSdkArchive(); } else { - throw new IllegalStateException("whaaat"); + throw new IllegalStateException( + "One and only one of SdkBundlePath and SdkArchivePath should be set."); } + return getOutputFile(); } private void validateInput() { @@ -310,6 +364,16 @@ private void validateInput() { checkFileExistsAndReadable(getSdkArchivePath().get()); checkFileHasExtension("ASAR file", getSdkArchivePath().get(), ".asar"); } + if (getOutputFormat().equals(APK_SET)) { + if (!Objects.equals(MoreFiles.getFileExtension(getOutputFile()), APK_SET_ARCHIVE_EXTENSION)) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Flag --output should be the path where to generate the APK Set. " + + "Its extension must be '.apks'.") + .build(); + } + checkFileDoesNotExist(getOutputFile()); + } } private void executeForSdkBundle() { diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManager.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManager.java index d7ff7cf0..5d77e459 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppManager.java @@ -23,9 +23,11 @@ import com.android.tools.build.bundletool.model.GeneratedApks; import com.android.tools.build.bundletool.model.GeneratedAssetSlices; import com.android.tools.build.bundletool.model.ModuleSplit; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.shards.ModuleSplitterForShards; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import java.nio.file.Path; import java.util.Optional; import javax.inject.Inject; @@ -59,14 +61,25 @@ void execute() { moduleSplitterForShards.generateSplits(module, /* shardingDimensions= */ ImmutableSet.of()); GeneratedApks generatedApks = GeneratedApks.fromModuleSplits(splits); - ApkSetWriter apkSetWriter = ApkSetWriter.zip(tempDirectory.getPath(), command.getOutputFile()); apkSerializerManager.serializeApkSetWithoutToc( - apkSetWriter, + createApkSetWriter(tempDirectory.getPath()), generatedApks, GeneratedAssetSlices.builder().build(), /* deviceSpec= */ Optional.empty(), LocalTestingInfo.getDefaultInstance(), /* permanentlyFusedModules= */ ImmutableSet.of()); } + + private ApkSetWriter createApkSetWriter(Path tempDir) { + switch (command.getOutputFormat()) { + case APK_SET: + return ApkSetWriter.zip(tempDir, command.getOutputFile()); + case DIRECTORY: + return ApkSetWriter.directory(command.getOutputFile()); + } + throw InvalidCommandException.builder() + .withInternalMessage("Unsupported output format '%s'.", command.getOutputFormat()) + .build(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java index b3e0f143..bebc0d29 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/ExtractApksCommand.java @@ -258,7 +258,11 @@ private void validateInput() { !getOutputDirectory().isPresent(), "Output directory should not be set when APKs are inside directory."); checkDirectoryExists(getApksArchivePath()); - checkFileExistsAndReadable(getApksArchivePath().resolve(FileNames.TABLE_OF_CONTENTS_FILE)); + Path tocFile = + Files.exists(getApksArchivePath().resolve(FileNames.TABLE_OF_CONTENTS_JSON_FILE)) + ? getApksArchivePath().resolve(FileNames.TABLE_OF_CONTENTS_JSON_FILE) + : getApksArchivePath().resolve(FileNames.TABLE_OF_CONTENTS_FILE); + checkFileExistsAndReadable(tocFile); } else { checkFileExistsAndReadable(getApksArchivePath()); } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java index 9d268a76..c66a3aed 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommand.java @@ -25,6 +25,7 @@ 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 static com.google.common.collect.Streams.stream; import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; @@ -88,6 +89,11 @@ public abstract class InstallMultiApksCommand { public static final String COMMAND_NAME = "install-multi-apks"; + public static final ImmutableMap NONUPDATABLE_PACKAGES_PAIRS = + ImmutableMap.of( + "com.google.android.ext.service", "com.google.android.extservice", + "com.google.android.permissioncontroller", "com.google.android.permission"); + private static final Flag ADB_PATH_FLAG = Flag.path("adb"); private static final Flag> APKS_ARCHIVES_FLAG = Flag.pathList("apks"); private static final Flag APKS_ARCHIVE_ZIP_FLAG = Flag.path("apks-zip"); @@ -494,17 +500,45 @@ private static ImmutableList extractApksFromZip(Path zipPath, TempDirector */ private static ImmutableList uniqueApksByPackageName( ImmutableList installableApksFiles) { - return installableApksFiles.stream() - .collect( - groupingBy( - PackagePathVersion::getPackageName, - maxBy(comparing(PackagePathVersion::getVersionCode)))) - .values() - .stream() - .flatMap(Streams::stream) + ImmutableList unfilteredResults = + installableApksFiles.stream() + .collect( + groupingBy( + PackagePathVersion::getPackageName, + maxBy(comparing(PackagePathVersion::getVersionCode)))) + .values() + .stream() + .flatMap(Streams::stream) + .collect(toImmutableList()); + + /* names of all the unique packages */ + ImmutableSet packageNames = + unfilteredResults.stream() + .map(PackagePathVersion::getPackageName) + .collect(toImmutableSet()); + + return unfilteredResults.stream() + .filter(result -> !isRedundantNonUpdatablePackage(result.getPackageName(), packageNames)) .collect(toImmutableList()); } + /** + * Check for the non-updatable package. If an updatable and non-updatable versions are present in + * the package list, only use the updatable version for the installation. + */ + static boolean isRedundantNonUpdatablePackage( + String packageName, ImmutableSet packageNames) { + + /* If non-updatable package and it's updatable pair is not present, return false. */ + if (!NONUPDATABLE_PACKAGES_PAIRS.containsKey(packageName)) { + return false; + } + + /* If updatable pair is present in the Apks list, return it for installation. */ + String name = NONUPDATABLE_PACKAGES_PAIRS.get(packageName); + return packageNames.contains(name); + } + public static CommandHelp help() { return CommandHelp.builder() .setCommandName(COMMAND_NAME) 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 ee3c1492..e4dbbde8 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 @@ -137,6 +137,11 @@ public abstract class AndroidManifest { 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 PATH_ATTRIBUTE_NAME = "path"; + public static final String PATH_PATTERN_NAME = "pathPattern"; + public static final String SCHEME_NAME = "scheme"; + public static final String HOST_NAME = "host"; + 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 = @@ -228,6 +233,11 @@ public abstract class AndroidManifest { public static final int SRC_RESOURCE_ID = 0x01010119; public static final int APP_COMPONENT_FACTORY_RESOURCE_ID = 0x0101057a; public static final int AUTHORITIES_RESOURCE_ID = 0x01010018; + public static final int PATH_RESOURCE_ID = 0x0101002a; + public static final int PATH_PATTERN_RESOURCE_ID = 0x0101002c; + public static final int PATH_PREFIX_RESOURCE_ID = 0x0101002b; + public static final int SCHEME_RESOURCE_ID = 0x01010027; + public static final int HOST_RESOURCE_ID = 0x01010028; // Matches the value of android.os.Build.VERSION_CODES.CUR_DEVELOPMENT, used when turning // a manifest attribute which references a prerelease API version (e.g., "Q") into an integer. @@ -837,7 +847,7 @@ public boolean isHeadless() { } public boolean hasMainActivity() { - return getActivities().stream() + return Stream.concat(getActivities().stream(), getActivityAliases().stream()) .flatMap(activity -> activity.getChildrenElements(INTENT_FILTER_ELEMENT_NAME)) .anyMatch( intent -> @@ -847,7 +857,7 @@ && hasChildWithNameAttribute( } public boolean hasMainTvActivity() { - return getActivities().stream() + return Stream.concat(getActivities().stream(), getActivityAliases().stream()) .flatMap(activity -> activity.getChildrenElements(INTENT_FILTER_ELEMENT_NAME)) .anyMatch( intent -> @@ -862,6 +872,12 @@ private ImmutableList getActivities() { .collect(toImmutableList()); } + private ImmutableList getActivityAliases() { + return stream(getManifestElement().getOptionalChildElement(APPLICATION_ELEMENT_NAME)) + .flatMap(app -> app.getChildrenElements(ACTIVITY_ALIAS_ELEMENT_NAME)) + .collect(toImmutableList()); + } + private static boolean hasChildWithNameAttribute( XmlProtoElement element, String childElementName, String nameAttributeValue) { return element diff --git a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java index f0592825..a766e275 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java +++ b/src/main/java/com/android/tools/build/bundletool/model/AppBundle.java @@ -77,6 +77,13 @@ public abstract class AppBundle implements Bundle { public static final String ARCHIVE_OPT_OUT_XML_PATH = "res/xml/com_android_vending_archive_opt_out.xml"; + /** + * If this file path exists in the base module of the app bundle, uncompressed dex optimisation + * will be disabled. + */ + public static final String UNCOMPRESSED_DEX_OPT_OUT_XML_PATH = + "res/xml/uncompressed_dex_opt_out.xml"; + /** Builds an {@link AppBundle} from an App Bundle on disk. */ public static AppBundle buildFromZip(ZipFile bundleFile) { BundleConfig bundleConfig = readBundleConfig(bundleFile); @@ -276,6 +283,14 @@ && getBaseModule() return Optional.empty(); } + public boolean getUncompressedDexOptOut() { + return hasBaseModule() + && getBaseModule() + .findEntriesUnderPath(BundleModule.RESOURCES_DIRECTORY) + .anyMatch( + entry -> entry.getPath().equals(ZipPath.create(UNCOMPRESSED_DEX_OPT_OUT_XML_PATH))); + } + /** Returns {@code true} if bundletool has to generate a LocaleConfig file. */ public boolean injectLocaleConfig() { return getBundleConfig().getLocales().getInjectLocaleConfig(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java index f0c10439..97140490 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java +++ b/src/main/java/com/android/tools/build/bundletool/model/BundleModule.java @@ -159,6 +159,12 @@ public AndroidManifest getAndroidManifest() { */ public abstract Optional getSdkModulesConfig(); + /** + * Package ID of resources of this module. Only set for modules of type SDK_DEPENDENCY_MODULE in + * app bundles. + */ + public abstract Optional getResourcesPackageId(); + /** * Returns entries of the module, indexed by their module path. * @@ -285,11 +291,8 @@ public ModuleMetadata getModuleMetadata(boolean isSdkRuntimeVariant) { .setDeliveryType(moduleDeliveryTypeToDeliveryType(getDeliveryType())); moduleTypeToFeatureModuleType(getModuleType()).ifPresent(moduleMetadata::setModuleType); - getSdkModulesConfig() - .ifPresent( - sdkModulesConfig -> - moduleMetadata.setSdkModuleMetadata( - sdkModulesConfigToSdkModuleMetadata(sdkModulesConfig))); + + getSdkModuleMetadata().ifPresent(moduleMetadata::setSdkModuleMetadata); if (isSdkRuntimeVariant) { getRuntimeEnabledSdkConfig() .ifPresent( @@ -301,6 +304,32 @@ public ModuleMetadata getModuleMetadata(boolean isSdkRuntimeVariant) { return moduleMetadata.build(); } + private Optional getSdkModuleMetadata() { + if (getModuleType().equals(ModuleType.SDK_DEPENDENCY_MODULE)) { + SdkModulesConfig sdkModulesConfig = + getSdkModulesConfig() + .orElseThrow( + () -> + new IllegalStateException( + "SDK_DEPENDENCY_MODULE does not have SdkModulesConfig set.")); + int resourcesPackageId = + getResourcesPackageId() + .orElseThrow( + () -> + new IllegalStateException( + "SDK_DEPENDENCY_MODULE does not have ResourcesPackageId set.")); + return Optional.of( + SdkModuleMetadata.newBuilder() + .setSdkPackageName(sdkModulesConfig.getSdkPackageName()) + .setSdkModuleVersion( + runtimeEnabledSdkVersionToModuleMetadataConverter( + sdkModulesConfig.getSdkVersion())) + .setResourcesPackageId(resourcesPackageId) + .build()); + } + return Optional.empty(); + } + private static ImmutableSet runtimeEnabledDependenciesFromConfig( RuntimeEnabledSdkConfig runtimeEnabledSdkConfig) { return runtimeEnabledSdkConfig.getRuntimeEnabledSdkList().stream() @@ -341,15 +370,6 @@ private static Optional moduleTypeToFeatureModuleType(ModuleT throw new IllegalArgumentException("Unknown module type: " + moduleType); } - private static SdkModuleMetadata sdkModulesConfigToSdkModuleMetadata( - SdkModulesConfig sdkModulesConfig) { - return SdkModuleMetadata.newBuilder() - .setSdkPackageName(sdkModulesConfig.getSdkPackageName()) - .setSdkModuleVersion( - runtimeEnabledSdkVersionToModuleMetadataConverter(sdkModulesConfig.getSdkVersion())) - .build(); - } - private static SdkModuleVersion runtimeEnabledSdkVersionToModuleMetadataConverter( RuntimeEnabledSdkVersion runtimeEnabledSdkVersion) { return SdkModuleVersion.newBuilder() @@ -395,6 +415,8 @@ public abstract Builder setRuntimeEnabledSdkConfig( public abstract Builder setSdkModulesConfig(SdkModulesConfig sdkModulesConfig); + public abstract Builder setResourcesPackageId(int resourcesPackageId); + public abstract Builder setModuleType(ModuleType moduleType); abstract ImmutableMap.Builder entryMapBuilder(); @@ -420,9 +442,7 @@ public Builder setRawEntries(Collection entries) { return this; } - /** - * @see #addEntry(ModuleEntry) - */ + /** See {@link #addEntry(ModuleEntry)}. */ @CanIgnoreReturnValue public Builder addEntries(Collection entries) { for (ModuleEntry entry : entries) { @@ -477,6 +497,10 @@ public final BundleModule build() { !bundleModule.getModuleType().equals(ModuleType.SDK_DEPENDENCY_MODULE) || bundleModule.getSdkModulesConfig().isPresent(), "BundleModule of type SDK_DEPENDENCY_MODULE can not have empty SdkModulesConfig."); + checkState( + !bundleModule.getModuleType().equals(ModuleType.SDK_DEPENDENCY_MODULE) + || bundleModule.getResourcesPackageId().isPresent(), + "BundleModule of type SDK_DEPENDENCY_MODULE can not have empty ResourcesPackageId."); return bundleModule; } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/FileNames.java b/src/main/java/com/android/tools/build/bundletool/model/utils/FileNames.java index 2016b3eb..0ffb7470 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/FileNames.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/FileNames.java @@ -25,5 +25,8 @@ public final class FileNames { */ public static final String TABLE_OF_CONTENTS_FILE = "toc.pb"; + /** Same as {@link TABLE_OF_CONTENTS_FILE}, but represented as JSON */ + public static final String TABLE_OF_CONTENTS_JSON_FILE = "toc.json"; + private FileNames() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java index 39f16dcd..765aae98 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ResultUtils.java @@ -17,8 +17,10 @@ package com.android.tools.build.bundletool.model.utils; import static com.android.tools.build.bundletool.model.utils.FileNames.TABLE_OF_CONTENTS_FILE; +import static com.android.tools.build.bundletool.model.utils.FileNames.TABLE_OF_CONTENTS_JSON_FILE; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.nio.charset.StandardCharsets.UTF_8; import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.ApkSet; @@ -26,10 +28,13 @@ import com.android.bundle.Commands.BuildSdkApksResult; import com.android.bundle.Commands.Variant; import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.version.BundleToolVersion; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Streams; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -43,11 +48,12 @@ public final class ResultUtils { public static BuildApksResult readTableOfContents(Path apksPath) { try { - if (Files.isDirectory(apksPath)) { - return readTableOfContentFromApksDirectory(apksPath); - } else { - return readTableOfContentFromApksArchive(apksPath); - } + ensureSingleToc(apksPath); + BuildApksResult result = + Files.isDirectory(apksPath) + ? readTableOfContentFromApksDirectory(apksPath) + : readTableOfContentFromApksArchive(apksPath); + return applyDefaultValues(result); } catch (IOException e) { throw new UncheckedIOException( String.format("Error while reading the table of contents file from '%s'.", apksPath), e); @@ -57,21 +63,67 @@ public static BuildApksResult readTableOfContents(Path apksPath) { private static BuildApksResult readTableOfContentFromApksArchive(Path apksArchivePath) throws IOException { try (ZipFile apksArchive = new ZipFile(apksArchivePath.toFile())) { + boolean tocJsonExists = apksArchive.getEntry(TABLE_OF_CONTENTS_JSON_FILE) != null; byte[] tocBytes = - ZipUtils.asByteSource(apksArchive, new ZipEntry(TABLE_OF_CONTENTS_FILE)).read(); - try { - return BuildApksResult.parseFrom(tocBytes); - } catch (InvalidProtocolBufferException e) { - // If loading the toc.pb into BuildApksResult fails, try to load it into BuildSdksApksResult - return toBuildApksResult(BuildSdkApksResult.parseFrom(tocBytes)); - } + tocJsonExists + ? ZipUtils.asByteSource(apksArchive, new ZipEntry(TABLE_OF_CONTENTS_JSON_FILE)).read() + : ZipUtils.asByteSource(apksArchive, new ZipEntry(TABLE_OF_CONTENTS_FILE)).read(); + return tocJsonExists ? parseJsonToc(tocBytes) : parseProtoToc(tocBytes); } } private static BuildApksResult readTableOfContentFromApksDirectory(Path apksDirectoryPath) throws IOException { - return BuildApksResult.parseFrom( - Files.readAllBytes(apksDirectoryPath.resolve(TABLE_OF_CONTENTS_FILE))); + boolean tocJsonExists = Files.exists(apksDirectoryPath.resolve(TABLE_OF_CONTENTS_JSON_FILE)); + byte[] tocBytes = + tocJsonExists + ? Files.readAllBytes(apksDirectoryPath.resolve(TABLE_OF_CONTENTS_JSON_FILE)) + : Files.readAllBytes(apksDirectoryPath.resolve(TABLE_OF_CONTENTS_FILE)); + return tocJsonExists ? parseJsonToc(tocBytes) : parseProtoToc(tocBytes); + } + + private static BuildApksResult parseJsonToc(byte[] bytes) throws IOException { + String jsonToc = new String(bytes, UTF_8); + BuildApksResult.Builder builder = BuildApksResult.newBuilder(); + JsonFormat.parser().ignoringUnknownFields().merge(jsonToc, builder); + return builder.build(); + } + + @VisibleForTesting + static BuildApksResult applyDefaultValues(BuildApksResult buildApksResult) { + BuildApksResult.Builder builder = buildApksResult.toBuilder(); + if (builder.getBundletool().getVersion().isEmpty()) { + builder.getBundletoolBuilder().setVersion(BundleToolVersion.getCurrentVersion().toString()); + } + return builder.build(); + } + + private static BuildApksResult parseProtoToc(byte[] bytes) throws IOException { + try { + return BuildApksResult.parseFrom(bytes); + } catch (InvalidProtocolBufferException e) { + // If loading the toc.pb into BuildApksResult fails, try to load it into BuildSdksApksResult + return toBuildApksResult(BuildSdkApksResult.parseFrom(bytes)); + } + } + + /* Ensures that an apks folder or zip has only one toc. */ + private static void ensureSingleToc(Path file) throws IOException { + if (Files.isDirectory(file)) { + if (Files.exists(file.resolve(TABLE_OF_CONTENTS_FILE)) + && Files.exists(file.resolve(TABLE_OF_CONTENTS_JSON_FILE))) { + throw new IllegalStateException( + "Apks directory cannot have both toc.pb and toc.json at the same time."); + } + } else { + try (ZipFile apksArchive = new ZipFile(file.toFile())) { + if (apksArchive.getEntry(TABLE_OF_CONTENTS_FILE) != null + && apksArchive.getEntry(TABLE_OF_CONTENTS_JSON_FILE) != null) { + throw new IllegalStateException( + "Apks archive cannot have both toc.pb and toc.json at the same time."); + } + } + } } public static ImmutableList splitApkVariants(BuildApksResult result) { @@ -121,17 +173,13 @@ public static ImmutableList archivedApkVariants(BuildApksResult result) } public static boolean isSplitApkVariant(Variant variant) { - return variant - .getApkSetList() - .stream() + return variant.getApkSetList().stream() .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) .allMatch(ApkDescription::hasSplitApkMetadata); } public static boolean isStandaloneApkVariant(Variant variant) { - return variant - .getApkSetList() - .stream() + return variant.getApkSetList().stream() .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) .allMatch(ApkDescription::hasStandaloneApkMetadata); } @@ -143,17 +191,13 @@ public static boolean isApexApkVariant(Variant variant) { } public static boolean isInstantApkVariant(Variant variant) { - return variant - .getApkSetList() - .stream() + return variant.getApkSetList().stream() .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) .allMatch(ApkDescription::hasInstantApkMetadata); } public static boolean isSystemApkVariant(Variant variant) { - return variant - .getApkSetList() - .stream() + return variant.getApkSetList().stream() .flatMap(apkSet -> apkSet.getApkDescriptionList().stream()) .anyMatch(ApkDescription::hasSystemApkMetadata); } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/ToXmlNode.java b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/ToXmlNode.java new file mode 100644 index 00000000..dba058e5 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/ToXmlNode.java @@ -0,0 +1,25 @@ +/* + * 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.utils.xmlproto; + +import com.android.aapt.Resources.XmlNode; + +/** Represents an entity that can be converted to {@link XmlNode}. */ +public interface ToXmlNode { + + /** Converts the entity to {@link XmlNode}. */ + XmlNode toXmlNode(); +} diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElement.java b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElement.java index 1bcf41ac..c107c230 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElement.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElement.java @@ -28,7 +28,8 @@ @Immutable public final class XmlProtoElement extends XmlProtoElementOrBuilder< - XmlNode, XmlProtoNode, XmlElement, XmlProtoElement, XmlAttribute, XmlProtoAttribute> { + XmlNode, XmlProtoNode, XmlElement, XmlProtoElement, XmlAttribute, XmlProtoAttribute> + implements ToXmlNode { private final XmlElement element; @@ -49,6 +50,11 @@ public XmlProtoElementBuilder toBuilder() { return new XmlProtoElementBuilder(element.toBuilder()); } + @Override + public XmlNode toXmlNode() { + return XmlNode.newBuilder().setElement(element).build(); + } + @Override public XmlElement getProto() { return element; diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElementBuilder.java b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElementBuilder.java index aa9dd247..45735360 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElementBuilder.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoElementBuilder.java @@ -24,6 +24,7 @@ import com.android.aapt.Resources.XmlElement; import com.android.aapt.Resources.XmlNamespace; import com.android.aapt.Resources.XmlNode; +import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import java.util.List; import java.util.function.Function; @@ -38,7 +39,8 @@ public final class XmlProtoElementBuilder XmlElement.Builder, XmlProtoElementBuilder, XmlAttribute.Builder, - XmlProtoAttributeBuilder> { + XmlProtoAttributeBuilder> + implements ToXmlNode { private final XmlElement.Builder element; @@ -64,6 +66,11 @@ public XmlElement.Builder getProto() { return element; } + @Override + public XmlNode toXmlNode() { + return XmlNode.newBuilder().setElement(element.build()).build(); + } + @Override protected List getProtoAttributesList() { return element.getAttributeBuilderList(); @@ -239,6 +246,26 @@ public XmlProtoElementBuilder removeChildrenElementsIf(Predicate children) { + children.forEach(child -> element.addChild(child.toXmlNode())); + return this; + } + + public XmlProtoElementBuilder clearAttribute() { + element.clearAttribute(); + return this; + } + + public XmlProtoElementBuilder addAllAttribute(ImmutableList attributes) { + attributes.forEach(attribute -> element.addAttribute(attribute.getProto())); + return this; + } + public XmlProtoElementBuilder modifyChildElements( Function mapper) { ImmutableList modifiedElements = diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNode.java b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNode.java index 9ec16119..42c94039 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNode.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNode.java @@ -23,8 +23,8 @@ /** Wrapper around the {@link XmlNode} proto, providing a fluent API. */ @Immutable -public final class XmlProtoNode - extends XmlProtoNodeOrBuilder { +public final class XmlProtoNode extends XmlProtoNodeOrBuilder + implements ToXmlNode { private final XmlNode node; @@ -44,6 +44,11 @@ public XmlProtoNodeBuilder toBuilder() { return new XmlProtoNodeBuilder(node.toBuilder()); } + @Override + public XmlNode toXmlNode() { + return node; + } + @Override public XmlNode getProto() { return node; diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNodeBuilder.java b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNodeBuilder.java index 312fd06b..27238564 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNodeBuilder.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/xmlproto/XmlProtoNodeBuilder.java @@ -22,7 +22,8 @@ /** Wrapper around the {@link XmlNode.Builder} proto, providing a fluent API. */ public final class XmlProtoNodeBuilder - extends XmlProtoNodeOrBuilder { + extends XmlProtoNodeOrBuilder + implements ToXmlNode { private final XmlNode.Builder node; @@ -47,6 +48,11 @@ public XmlProtoNode build() { return new XmlProtoNode(node.build()); } + @Override + public XmlNode toXmlNode() { + return node.build(); + } + @Override protected XmlElement.Builder getProtoElement() { return node.getElementBuilder(); 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 bb325388..90d52a29 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.15.1"; + private static final String CURRENT_VERSION = "1.15.2"; /** Returns the version of BundleTool being run. */ public static Version getCurrentVersion() { diff --git a/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java b/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java index ddb658b8..0d7ce238 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java +++ b/src/main/java/com/android/tools/build/bundletool/model/version/VersionGuardedFeature.java @@ -93,7 +93,19 @@ public enum VersionGuardedFeature { * Install time modules will be merged into base unless explicitly turned off via in "install-time" attribute. */ - MERGE_INSTALL_TIME_MODULES_INTO_BASE("1.0.0"); + MERGE_INSTALL_TIME_MODULES_INTO_BASE("1.0.0"), + + /** + * Fix empty density splits from being generated. + * + *

Density splits used to be generated even if there are no density specific resources in the + * feature. This caused density splits with empty res/ directories and empty resource.arsc tables + * to be generated. + * + *

Now, features with no density specific resources (or with a single density specific + * resource) will not have density splits generated for them. + */ + FIX_SKIP_GENERATING_EMPTY_DENSITY_SPLITS("1.15.1"); /** Version from which the given feature should be enabled by default. */ private final Version enabledSinceVersion; diff --git a/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java b/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java index 94f001ff..7d1c2ec6 100644 --- a/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java +++ b/src/main/java/com/android/tools/build/bundletool/sdkmodule/DexAndResourceRepackager.java @@ -103,7 +103,10 @@ BundleModule repackage(BundleModule module) { sdkModulesConfig.getSdkPackageName()); module = dexRepackager.applyMutation(module); module = javaResourceRepackager.applyMutation(module); - return module.toBuilder().addEntry(getCompatSdkConfigModuleEntry(module)).build(); + return module.toBuilder() + .addEntry(getCompatSdkConfigModuleEntry(module)) + .setResourcesPackageId(inheritedAppProperties.getResourcesPackageId()) + .build(); } /** diff --git a/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java b/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java index b68374ef..cd5894e2 100644 --- a/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java +++ b/src/main/java/com/android/tools/build/bundletool/sdkmodule/SdkModuleToAppBundleModuleConverter.java @@ -70,9 +70,9 @@ public SdkModuleToAppBundleModuleConverter( */ public BundleModule convert() { return renameAndroidResources( - repackageDexAndJavaResources( - remapResourceIdsInResourceTable( - remapResourceIdsInXmlResources(convertNameTypeAndManifest(sdkModule))))); + convertNameTypeAndManifest( + repackageDexAndJavaResources( + remapResourceIdsInResourceTable(remapResourceIdsInXmlResources(sdkModule))))); } private BundleModule remapResourceIdsInResourceTable(BundleModule module) { diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java b/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java index a573181d..8fc98fed 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitter.java @@ -21,6 +21,7 @@ import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.DEFAULT_DENSITY_VALUE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.MIPMAP_TYPE; import static com.android.tools.build.bundletool.model.utils.ResourcesUtils.getLowestDensity; +import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.FIX_SKIP_GENERATING_EMPTY_DENSITY_SPLITS; import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.RESOURCES_WITH_NO_ALTERNATIVES_IN_MASTER_SPLIT; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; @@ -116,10 +117,15 @@ public ImmutableCollection splitInternal(ModuleSplit split) { ImmutableList.Builder splitsBuilder = new ImmutableList.Builder<>(); for (DensityAlias density : densityBuckets) { ResourceTable optimizedTable = filterResourceTableForDensity(resourceTable.get(), density); + // Don't generate empty splits. - if (optimizedTable.equals(ResourceTable.getDefaultInstance())) { + if (FIX_SKIP_GENERATING_EMPTY_DENSITY_SPLITS.enabledForVersion(bundleVersion) + && optimizedTable.getPackageList().isEmpty()) { + continue; + } else if (optimizedTable.equals(ResourceTable.getDefaultInstance())) { continue; } + ModuleSplit.Builder moduleSplitBuilder = split.toBuilder() .setApkTargeting( diff --git a/src/main/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidator.java index f9b194f0..c1fdc8f2 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidator.java @@ -65,7 +65,7 @@ private void validateDwfBundle(AppBundle bundle) { validateBaseHasNoExecutableComponents(baseModule); validateBaseHasNoLibs(baseModule); - validateBaseHasNoRoot(baseModule); + validateBaseHasNoCodeInRoot(baseModule); Optional> optionalRuntime = getOptionalRuntime(bundle); @@ -183,10 +183,17 @@ private void validateBaseHasNoLibs(BundleModule baseModule) { assertWithUserMessage(!hasLibs, "Watch face cannot have any external libraries."); } - private void validateBaseHasNoRoot(BundleModule baseModule) { - boolean hasRoot = - baseModule.findEntriesUnderPath(BundleModule.ROOT_DIRECTORY).findFirst().isPresent(); - assertWithUserMessage(!hasRoot, "Watch face cannot have any files in the root of the package."); + private void validateBaseHasNoCodeInRoot(BundleModule baseModule) { + boolean hasCodeInRoot = + baseModule + .findEntriesUnderPath(BundleModule.ROOT_DIRECTORY) + .anyMatch( + entry -> { + String fileName = entry.getPath().toString(); + return fileName.endsWith(".so") || fileName.endsWith(".dex"); + }); + assertWithUserMessage( + !hasCodeInRoot, "Watch face cannot have any compiled code in its root folder."); } private void assertWithUserMessage(boolean condition, String message) { diff --git a/src/main/proto/commands.proto b/src/main/proto/commands.proto index fc18fcdb..77608b58 100644 --- a/src/main/proto/commands.proto +++ b/src/main/proto/commands.proto @@ -335,11 +335,17 @@ message RuntimeEnabledSdkDependency { // depends on. message SdkModuleMetadata { // Version of the Runtime-enabled SDK that this module was generated from. + // Required. optional SdkModuleVersion sdk_module_version = 1; // Package name of the runtime-enabled SDK that this module was generated // from. + // Required. optional string sdk_package_name = 2; + + // Package ID of the resources of this module. Can have values between 2-255. + // Required. + optional int32 resources_package_id = 3; } // Versioning information about the SDK that the app module was generated from. diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java index 989a09d6..e0ad5c37 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksDeviceSpecTest.java @@ -250,35 +250,7 @@ public void deviceSpec_systemApkMode_partialDeviceSpecMissingAbi_throws() throws Throwable exception = assertThrows(InvalidCommandException.class, command::build); assertThat(exception) .hasMessageThat() - .contains( - "Device spec must have screen density and ABIs set when running with 'system' mode" - + " flag."); - } - - @Test - @Theory - public void deviceSpec_systemApkMode_partialDeviceSpecMissingDensity_throws() throws Exception { - DeviceSpec deviceSpec = mergeSpecs(abis("arm64-v8a")); - - AppBundle appBundle = - new AppBundleBuilder() - .addModule("base", module -> module.setManifest(androidManifest("com.app"))) - .build(); - bundleSerializer.writeToDisk(appBundle, bundlePath); - - BuildApksCommand.Builder command = - BuildApksCommand.builder() - .setBundlePath(bundlePath) - .setOutputFile(outputFilePath) - .setDeviceSpec(deviceSpec) - .setApkBuildMode(SYSTEM); - - Throwable exception = assertThrows(InvalidCommandException.class, command::build); - assertThat(exception) - .hasMessageThat() - .contains( - "Device spec must have screen density and ABIs set when running with 'system' mode" - + " flag."); + .contains("Device spec must have ABIs set when running with 'system' mode flag."); } @Test 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 c7a738e8..49e1fd2a 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 @@ -84,6 +84,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSdkVersion; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTitle; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withUsesSplit; +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.TEST_LABEL_RESOURCE_ID; @@ -1073,6 +1074,72 @@ public void multipleModulesFusedAndNotFused_systemApks_hasCorrectAdditionalLangu .forEach(apkDescription -> assertThat(apkSetFile).hasFile(apkDescription.getPath())); } + @Test + public void buildApksCommand_system_noDensityInSpec_includesAllDensities() throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("res/drawable-ldpi/image.jpg") + .addFile("res/drawable-mdpi/image.jpg") + .addFile("res/drawable-hdpi/image.jpg") + .setResourceTable( + new ResourceTableBuilder() + .addPackage("com.test.app") + .addFileResourceForMultipleConfigs( + "drawable", + "image", + ImmutableMap.of( + LDPI, + "res/drawable-ldpi/image.jpg", + MDPI, + "res/drawable-mdpi/image.jpg", + HDPI, + "res/drawable-hdpi/image.jpg")) + .build()) + .setManifest(androidManifest("com.test.app"))) + .build(); + TestComponent.useTestModule( + this, + createTestModuleBuilder() + .withAppBundle(appBundle) + .withOutputPath(outputFilePath) + .withApkBuildMode(SYSTEM) + .withDeviceSpec(mergeSpecs(sdkVersion(28), abis("x86"), locales("en-US"))) + .build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + + // Should not shard by screen density. + assertThat(systemApkVariants(result)).hasSize(1); + + Variant systemVariant = systemApkVariants(result).get(0); + // No screen density targeting. + assertThat(systemVariant.getTargeting().hasScreenDensityTargeting()).isFalse(); + + assertThat(apkDescriptions(systemVariant)).hasSize(1); + ApkDescription systemApk = apkDescriptions(systemVariant).get(0); + assertThat(systemApk.getTargeting().hasScreenDensityTargeting()).isFalse(); + + File systemApkFile = extractFromApkSetFile(apkSetFile, systemApk.getPath(), outputDir); + try (ZipFile systemApkZipFile = new ZipFile(systemApkFile)) { + // "res/xml/splits0.xml" is created by bundletool with list of generated splits. + TruthZip.assertThat(systemApkZipFile) + .containsExactlyEntries( + "AndroidManifest.xml", + "res/drawable-hdpi/image.jpg", + "res/drawable-ldpi/image.jpg", + "res/drawable-mdpi/image.jpg", + "res/xml/splits0.xml", + "resources.arsc"); + } + } + @Test public void buildApksCommand_appTargetingAllSdks_buildsSplitAndStandaloneApks() throws Exception { AppBundle appBundle = @@ -2937,6 +3004,43 @@ public void enabledDexCompressionSplitter_disabledUncompressedDex_noUncompressed } + @Test + public void + uncompressedDexOptOut_withEnabledUncompressDexInBundleConfig_noUncompressedDexVariant() + throws Exception { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + builder -> + builder + .addFile("dex/classes.dex") + .addFile( + AppBundle.UNCOMPRESSED_DEX_OPT_OUT_XML_PATH, + TestData.readBytes("testdata/xml/opt-out.xml")) + .setManifest(androidManifest("com.test.app")) + .setResourceTable( + new ResourceTableBuilder() + .addPackage("com.test.app") + .addXmlResource( + "optout", AppBundle.UNCOMPRESSED_DEX_OPT_OUT_XML_PATH) + .build())) + .setBundleConfig(BundleConfigBuilder.create().setUncompressDexFiles(true).build()) + .build(); + TestComponent.useTestModule( + this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); + + buildApksManager.execute(); + + ZipFile apkSetFile = openZipFile(outputFilePath.toFile()); + BuildApksResult result = extractTocFromApkSetFile(apkSetFile, outputDir); + ImmutableList splitApkVariants = splitApkVariants(result); + assertThat( + splitApkVariants.stream() + .map(variant -> variant.getTargeting().getSdkVersionTargeting())) + .containsExactly(sdkVersionTargeting(L_SDK_VERSION, ImmutableSet.of(LOWEST_SDK_VERSION))); + } + @Test public void dexCompressionIsNotSet_enabledByDefault() throws Exception { AppBundle appBundle = diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommandTest.java index d13a8d4d..83a0dcb1 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksForAppCommandTest.java @@ -40,6 +40,7 @@ import com.android.bundle.RuntimeEnabledSdkConfigProto.SdkSplitPropertiesInheritedFromApp; import com.android.bundle.SdkMetadataOuterClass.SdkMetadata; import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; +import com.android.tools.build.bundletool.commands.BuildApksCommand.OutputFormat; import com.android.tools.build.bundletool.flags.FlagParser; import com.android.tools.build.bundletool.io.AppBundleSerializer; import com.android.tools.build.bundletool.io.ZipBuilder; @@ -48,6 +49,7 @@ import com.android.tools.build.bundletool.model.SigningConfiguration; import com.android.tools.build.bundletool.model.ZipPath; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.utils.SystemEnvironmentProvider; import com.android.tools.build.bundletool.model.utils.ZipUtils; import com.android.tools.build.bundletool.testing.ApkSetUtils; @@ -233,6 +235,36 @@ public void buildingViaFlagsAndBuilderHasSameResult_optionalSigning() { assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); } + @Test + public void buildingViaFlagsAndBuilderHasSameResult_withDirectoryOutputFormat() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BuildSdkApksForAppCommand commandViaFlags = + BuildSdkApksForAppCommand.fromFlags( + new FlagParser() + .parse( + "--sdk-archive=" + sdkAsarPath, + "--app-properties=" + inheritedAppPropertiesConfigPath, + "--output=" + tmpDir, + "--output-format=" + OutputFormat.DIRECTORY, + "--aapt2=" + AAPT2_PATH), + new PrintStream(output), + systemEnvironmentProvider); + + BuildSdkApksForAppCommand.Builder commandViaBuilder = + BuildSdkApksForAppCommand.builder() + .setSdkArchivePath(sdkAsarPath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(tmpDir) + .setOutputFormat(OutputFormat.DIRECTORY) + .setAapt2Command(commandViaFlags.getAapt2Command().get()) + .setExecutorService(commandViaFlags.getExecutorService()) + .setExecutorServiceCreatedByBundleTool(true); + DebugKeystoreUtils.getDebugSigningConfiguration(systemEnvironmentProvider) + .ifPresent(commandViaBuilder::setSigningConfiguration); + + assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); + } + @Test public void sdkAsarNotSet_sdkBundleNotSet_throws() { Throwable exceptionFromBuilder = @@ -409,6 +441,43 @@ public void sdkBundleSet_badExtension_throws() throws Exception { assertThat(exceptionFromFlags).hasMessageThat().contains("expected to have '.asb' extension."); } + @Test + public void outputFileHasBadExtension_throws() throws Exception { + Path outputFilePathWithBadExtension = tmpDir.resolve("apks.txt"); + createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), extractedModulesFilePath) + .writeTo(sdkBundlePath); + BuildSdkApksForAppCommand command = + BuildSdkApksForAppCommand.builder() + .setSdkBundlePath(sdkBundlePath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePathWithBadExtension) + .build(); + + Throwable e = assertThrows(InvalidCommandException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains( + "Flag --output should be the path where to generate the APK Set. Its extension must be" + + " '.apks'."); + } + + @Test + public void outputFileExists_throws() throws Exception { + // create file on the output path. + createZipBuilderForModules().writeTo(outputFilePath); + createZipBuilderForSdkBundleWithModules(createZipBuilderForModules(), extractedModulesFilePath) + .writeTo(sdkBundlePath); + BuildSdkApksForAppCommand command = + BuildSdkApksForAppCommand.builder() + .setSdkBundlePath(sdkBundlePath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(outputFilePath) + .build(); + + Throwable e = assertThrows(IllegalArgumentException.class, command::execute); + assertThat(e).hasMessageThat().contains("already exists."); + } + @Test public void modulesZipMissingManifestInAsar_validationFails() throws Exception { createZipBuilderForSdkAsarWithModules( @@ -617,6 +686,27 @@ public void generateModuleSplit_withSdkBundle_sameAsBuildApks() throws Exception assertThat(getFileHash(buildSdkApksForAppOutputApk)).isEqualTo(getFileHash(buildApksOutputApk)); } + @Test + public void directoryOutputFormat_success() throws Exception { + ZipBuilder sdkBundleZipBuilder = + createZipBuilderForSdkBundleWithModules( + createZipBuilderForModules(), extractedModulesFilePath); + sdkBundleZipBuilder.writeTo(sdkBundlePath); + BuildSdkApksForAppCommand command = + BuildSdkApksForAppCommand.builder() + .setSdkBundlePath(sdkBundlePath) + .setInheritedAppProperties(INHERITED_APP_PROPERTIES) + .setOutputFile(tmpDir) + .setOutputFormat(OutputFormat.DIRECTORY) + .build(); + + command.execute(); + + String sdkSplitPath = + "splits/" + SdkBundleBuilder.PACKAGE_NAME.replace(".", "") + "-master.apk"; + assertThat(Files.exists(Path.of(tmpDir.toString(), sdkSplitPath))).isTrue(); + } + @Test public void printHelpDoesNotCrash() { BuildSdkApksForAppCommand.help(); diff --git a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java index be8c3fb4..23d6b70d 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/ExtractApksCommandTest.java @@ -100,6 +100,7 @@ import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; import com.android.tools.build.bundletool.model.exceptions.InvalidDeviceSpecException; import com.android.tools.build.bundletool.model.version.BundleToolVersion; +import com.android.tools.build.bundletool.testing.ApksArchiveHelpers.TocFormat; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -135,9 +136,11 @@ public void setUp() throws Exception { tmpDir = tmp.getRoot().toPath(); } + @Theory @Test - public void missingDeviceSpecFlag_throws() throws Exception { - Path apksArchiveFile = createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks")); + public void missingDeviceSpecFlag_throws(TocFormat tocFormat) throws Exception { + Path apksArchiveFile = + createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks"), tocFormat); expectMissingRequiredFlagException( "device-spec", @@ -172,9 +175,11 @@ public void nonExistentApksArchiveFile_throws() throws Exception { assertThat(exception).hasMessageThat().contains("File 'nonexistent' was not found"); } + @Theory @Test - public void nonExistentDeviceSpecFile_throws() throws Exception { - Path apksArchiveFile = createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks")); + public void nonExistentDeviceSpecFile_throws(TocFormat tocFormat) throws Exception { + Path apksArchiveFile = + createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks"), tocFormat); Throwable exception = assertThrows( @@ -187,9 +192,11 @@ public void nonExistentDeviceSpecFile_throws() throws Exception { assertThat(exception).hasMessageThat().contains("File 'not-found.pb' was not found"); } + @Theory @Test - public void outputDirectoryCreatedIfDoesNotExist() throws Exception { - Path apksArchivePath = createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks")); + public void outputDirectoryCreatedIfDoesNotExist(TocFormat tocFormat) throws Exception { + Path apksArchivePath = + createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); Path outputDirectory = tmpDir.resolve("directory-that-does-not-exist"); @@ -224,8 +231,9 @@ public void outputDirectorySetWhenUsingDirectory_throws() throws Exception { .contains("Output directory should not be set when APKs are inside directory"); } + @Theory @Test - public void permanentlyMergedModule() throws Exception { + public void permanentlyMergedModule(TocFormat tocFormat) throws Exception { ZipPath apkLBase = ZipPath.create("apkL-base.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() @@ -243,7 +251,7 @@ public void permanentlyMergedModule() throws Exception { .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); Path deviceSpecFile = createDeviceSpecFile(deviceWithSdk(21), tmpDir.resolve("device.json")); ExtractApksCommand command = ExtractApksCommand.fromFlags( @@ -258,8 +266,9 @@ public void permanentlyMergedModule() throws Exception { .containsExactly("apkL-base.apk"); } + @Theory @Test - public void nonExistentModule_throws() throws Exception { + public void nonExistentModule_throws(TocFormat tocFormat) throws Exception { ZipPath apkLBase = ZipPath.create("apkL-base.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() @@ -275,7 +284,7 @@ public void nonExistentModule_throws() throws Exception { .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); Path deviceSpecFile = createDeviceSpecFile(deviceWithSdk(21), tmpDir.resolve("device.json")); ExtractApksCommand command = ExtractApksCommand.fromFlags( @@ -292,9 +301,11 @@ public void nonExistentModule_throws() throws Exception { .contains("The APK Set archive does not contain the following modules: [unknown_module]"); } + @Theory @Test - public void emptyModules_throws() throws Exception { - Path apksArchiveFile = createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks")); + public void emptyModules_throws(TocFormat tocFormat) throws Exception { + Path apksArchiveFile = + createApksArchiveFile(minimalApkSet(), tmpDir.resolve("bundle.apks"), tocFormat); Path deviceSpecFile = createDeviceSpecFile(deviceWithSdk(21), tmpDir.resolve("device.json")); ExtractApksCommand command = ExtractApksCommand.fromFlags( @@ -307,8 +318,9 @@ public void emptyModules_throws() throws Exception { assertThat(exception).hasMessageThat().contains("The set of modules cannot be empty."); } + @Theory @Test - public void deviceSpecFromPbJson() throws Exception { + public void deviceSpecFromPbJson(TocFormat tocFormat) throws Exception { DeviceSpec.Builder expectedDeviceSpecBuilder = DeviceSpec.newBuilder(); try (Reader reader = TestData.openReader("testdata/device/pixel2_spec.json")) { JsonFormat.parser().merge(reader, expectedDeviceSpecBuilder); @@ -317,7 +329,7 @@ public void deviceSpecFromPbJson() throws Exception { BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); Path deviceSpecFile = TestData.copyToTempDir(tmp, "testdata/device/pixel2_spec.json"); ExtractApksCommand command = @@ -327,13 +339,14 @@ public void deviceSpecFromPbJson() throws Exception { assertThat(command.getDeviceSpec()).isEqualTo(expectedDeviceSpec); } + @Theory @Test - public void deviceSpecUnknownExtension_throws() throws Exception { + public void deviceSpecUnknownExtension_throws(TocFormat tocFormat) throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("bad_filename.dat")); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); Throwable exception = assertThrows( @@ -346,12 +359,13 @@ public void deviceSpecUnknownExtension_throws() throws Exception { assertThat(exception).hasMessageThat().contains("Expected .json extension for the device spec"); } + @Theory @Test - public void deviceSpecViaJavaApi_invalid_throws() throws Exception { + public void deviceSpecViaJavaApi_invalid_throws(TocFormat tocFormat) throws Exception { DeviceSpec invalidDeviceSpec = deviceWithSdk(-1); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); Throwable exception = assertThrows( @@ -365,13 +379,15 @@ public void deviceSpecViaJavaApi_invalid_throws() throws Exception { assertThat(exception).hasMessageThat().contains("Device spec SDK version"); } + @Theory @Test - public void builderAndFlagsConstruction_inJavaViaProtos_equivalent() throws Exception { + public void builderAndFlagsConstruction_inJavaViaProtos_equivalent(TocFormat tocFormat) + throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); ExtractApksCommand fromFlags = ExtractApksCommand.fromFlags( @@ -386,12 +402,14 @@ public void builderAndFlagsConstruction_inJavaViaProtos_equivalent() throws Exce assertThat(fromFlags).isEqualTo(fromBuilderApi); } + @Theory @Test - public void builderAndFlagsConstruction_inJavaViaFiles_equivalent() throws Exception { + public void builderAndFlagsConstruction_inJavaViaFiles_equivalent(TocFormat tocFormat) + throws Exception { Path deviceSpecFile = createDeviceSpecFile(deviceWithSdk(21), tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); ExtractApksCommand fromFlags = ExtractApksCommand.fromFlags( @@ -406,13 +424,15 @@ public void builderAndFlagsConstruction_inJavaViaFiles_equivalent() throws Excep assertThat(fromFlags).isEqualTo(fromBuilderApi); } + @Theory @Test - public void builderAndFlagsConstruction_optionalModules_equivalent() throws Exception { + public void builderAndFlagsConstruction_optionalModules_equivalent(TocFormat tocFormat) + throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); ExtractApksCommand fromFlags = ExtractApksCommand.fromFlags( @@ -433,13 +453,15 @@ public void builderAndFlagsConstruction_optionalModules_equivalent() throws Exce assertThat(fromFlags).isEqualTo(fromBuilderApi); } + @Theory @Test - public void builderAndFlagsConstruction_optionalInstant_equivalent() throws Exception { + public void builderAndFlagsConstruction_optionalInstant_equivalent(TocFormat tocFormat) + throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); ExtractApksCommand fromFlags = ExtractApksCommand.fromFlags( @@ -460,13 +482,15 @@ public void builderAndFlagsConstruction_optionalInstant_equivalent() throws Exce assertThat(fromFlags).isEqualTo(fromBuilderApi); } + @Theory @Test - public void builderAndFlagsConstruction_optionalInstantFalse_equivalent() throws Exception { + public void builderAndFlagsConstruction_optionalInstantFalse_equivalent(TocFormat tocFormat) + throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); ExtractApksCommand fromFlags = ExtractApksCommand.fromFlags( @@ -483,12 +507,14 @@ public void builderAndFlagsConstruction_optionalInstantFalse_equivalent() throws } @Test - public void builderAndFlagsConstruction_optionalOutputDirectory_equivalent() throws Exception { + @Theory + public void builderAndFlagsConstruction_optionalOutputDirectory_equivalent(TocFormat tocFormat) + throws Exception { DeviceSpec deviceSpec = deviceWithSdk(21); Path deviceSpecFile = createDeviceSpecFile(deviceSpec, tmpDir.resolve("device.json")); BuildApksResult tableOfContentsProto = minimalApkSet(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); ExtractApksCommand fromFlags = ExtractApksCommand.fromFlags( @@ -515,7 +541,8 @@ public void builderAndFlagsConstruction_optionalOutputDirectory_equivalent() thr @Test @Theory public void oneModule_Ldevice_matchesLmasterSplit( - @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + @FromDataPoints("apksInDirectory") boolean apksInDirectory, TocFormat tocFormat) + throws Exception { ZipPath apkOne = ZipPath.create("apk_one.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() @@ -528,7 +555,7 @@ public void oneModule_Ldevice_matchesLmasterSplit( ApkTargeting.getDefaultInstance(), apkOne)) .build(); - Path apksPath = createApks(tableOfContentsProto, apksInDirectory); + Path apksPath = createApks(tableOfContentsProto, apksInDirectory, tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -552,7 +579,8 @@ public void oneModule_Ldevice_matchesLmasterSplit( @Test @Theory public void oneModule_Mdevice_matchesMSplit( - @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + @FromDataPoints("apksInDirectory") boolean apksInDirectory, TocFormat tocFormat) + throws Exception { ZipPath apkPreL = ZipPath.create("standalones/apkPreL.apk"); ZipPath apkL = ZipPath.create("splits/apkL.apk"); ZipPath apkM = ZipPath.create("splits/apkM.apk"); @@ -583,7 +611,7 @@ public void oneModule_Mdevice_matchesMSplit( ApkTargeting.getDefaultInstance(), apkM)) .build(); - Path apksPath = createApks(tableOfContentsProto, apksInDirectory); + Path apksPath = createApks(tableOfContentsProto, apksInDirectory, tocFormat); DeviceSpec deviceSpec = deviceWithSdk(23); @@ -607,7 +635,8 @@ public void oneModule_Mdevice_matchesMSplit( @Test @Theory public void oneModule_Kdevice_matchesPreLSplit( - @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + @FromDataPoints("apksInDirectory") boolean apksInDirectory, TocFormat tocFormat) + throws Exception { ZipPath apkPreL = ZipPath.create("standalones/apkPreL.apk"); ZipPath apkL = ZipPath.create("splits/apkL.apk"); ZipPath apkM = ZipPath.create("splits/apkM.apk"); @@ -639,7 +668,7 @@ public void oneModule_Kdevice_matchesPreLSplit( apkM)) .build(); - Path apksPath = createApks(tableOfContentsProto, apksInDirectory); + Path apksPath = createApks(tableOfContentsProto, apksInDirectory, tocFormat); DeviceSpec deviceSpec = deviceWithSdk(19); @@ -663,7 +692,8 @@ public void oneModule_Kdevice_matchesPreLSplit( @Test @Theory public void oneModule_Ldevice_matchesLSplit( - @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + @FromDataPoints("apksInDirectory") boolean apksInDirectory, TocFormat tocFormat) + throws Exception { ZipPath apkPreL = ZipPath.create("standalones/apkPreL.apk"); ZipPath apkL = ZipPath.create("splits/apkL.apk"); ZipPath apkM = ZipPath.create("splits/apkM.apk"); @@ -695,7 +725,7 @@ public void oneModule_Ldevice_matchesLSplit( apkM)) .build(); - Path apksPath = createApks(tableOfContentsProto, apksInDirectory); + Path apksPath = createApks(tableOfContentsProto, apksInDirectory, tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); ExtractApksCommand.Builder extractedApksCommand = @@ -715,8 +745,9 @@ public void oneModule_Ldevice_matchesLSplit( } } + @Theory @Test - public void apexModule_noMatch() throws Exception { + public void apexModule_noMatch(TocFormat tocFormat) throws Exception { BuildApksResult buildApksResult = BuildApksResult.newBuilder() .setBundletool( @@ -727,7 +758,8 @@ public void apexModule_noMatch() throws Exception { multiAbiTargeting(X86_64), ZipPath.create("standalones/standalone-x86_64.apk"))) .build(); - Path apksPath = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksPath = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); ExtractApksCommand.Builder extractedApksCommand = ExtractApksCommand.builder().setApksArchivePath(apksPath).setDeviceSpec(abis("x86")); @@ -744,7 +776,8 @@ public void apexModule_noMatch() throws Exception { @Test @Theory public void apexModule_getsBestPossibleApk( - @FromDataPoints("apksInDirectory") boolean apksInDirectory) throws Exception { + @FromDataPoints("apksInDirectory") boolean apksInDirectory, TocFormat tocFormat) + throws Exception { ZipPath x64Apk = ZipPath.create("standalones/standalone-x86_64.apk"); ZipPath x64X86Apk = ZipPath.create("standalones/standalone-x86_64.x86.apk"); ZipPath x64ArmApk = ZipPath.create("standalones/standalone-x86_64.arm64_v8a.apk"); @@ -772,7 +805,7 @@ public void apexModule_getsBestPossibleApk( .addVariant(multiAbiTargetingApexVariant(x64ArmTargeting, x64ArmApk)) .build(); - Path apksPath = createApks(buildApksResult, apksInDirectory); + Path apksPath = createApks(buildApksResult, apksInDirectory, tocFormat); ExtractApksCommand.Builder extractedApksCommand = ExtractApksCommand.builder() .setApksArchivePath(apksPath) @@ -793,8 +826,9 @@ public void apexModule_getsBestPossibleApk( } } + @Theory @Test - public void oneModule_Kdevice_noMatchingSdkVariant_throws() throws Exception { + public void oneModule_Kdevice_noMatchingSdkVariant_throws(TocFormat tocFormat) throws Exception { ZipPath apkL = ZipPath.create("splits/apkL.apk"); ZipPath apkM = ZipPath.create("splits/apkM.apk"); BuildApksResult tableOfContentsProto = @@ -814,7 +848,7 @@ public void oneModule_Kdevice_noMatchingSdkVariant_throws() throws Exception { apkM)) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(19); @@ -830,8 +864,9 @@ public void oneModule_Kdevice_noMatchingSdkVariant_throws() throws Exception { .contains("SDK version (19) of the device is not supported."); } + @Theory @Test - public void oneModule_MipsDevice_noMatchingAbiSplit_throws() throws Exception { + public void oneModule_MipsDevice_noMatchingAbiSplit_throws(TocFormat tocFormat) throws Exception { ZipPath apkL = ZipPath.create("splits/apkL.apk"); ZipPath apkLx86 = ZipPath.create("splits/apkL-x86.apk"); BuildApksResult tableOfContentsProto = @@ -851,7 +886,7 @@ public void oneModule_MipsDevice_noMatchingAbiSplit_throws() throws Exception { /* isMasterSplit= */ false)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = mergeSpecs(sdkVersion(21), abis("arm64-v8a"), locales("en-US"), density(DensityAlias.HDPI)); @@ -870,8 +905,9 @@ public void oneModule_MipsDevice_noMatchingAbiSplit_throws() throws Exception { + "app ABIs: [x86]"); } + @Theory @Test - public void oneModule_extractedToTemporaryDirectory() throws Exception { + public void oneModule_extractedToTemporaryDirectory(TocFormat tocFormat) throws Exception { ZipPath apkOne = ZipPath.create("apk_one.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() @@ -885,7 +921,7 @@ public void oneModule_extractedToTemporaryDirectory() throws Exception { apkOne)) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -907,8 +943,10 @@ public void oneModule_extractedToTemporaryDirectory() throws Exception { assertThat(Iterables.getOnlyElement(matchedApks).toString()).startsWith(apkOnePathPrefix); } + @Theory @Test - public void twoModules_Ldevice_matchesLSplitsForSpecifiedModules() throws Exception { + public void twoModules_Ldevice_matchesLSplitsForSpecifiedModules(TocFormat tocFormat) + throws Exception { ZipPath apkPreL = ZipPath.create("apkPreL.apk"); ZipPath apkLBase = ZipPath.create("apkL-base.apk"); ZipPath apkLFeature = ZipPath.create("apkL-feature.apk"); @@ -942,7 +980,7 @@ public void twoModules_Ldevice_matchesLSplitsForSpecifiedModules() throws Except createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkLOther)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -962,8 +1000,9 @@ public void twoModules_Ldevice_matchesLSplitsForSpecifiedModules() throws Except } } + @Theory @Test - public void moduleWithDependency_extractDependency() throws Exception { + public void moduleWithDependency_extractDependency(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("base-master.apk"); ZipPath apkFeature1 = ZipPath.create("feature1-master.apk"); ZipPath apkFeature2 = ZipPath.create("feature2-master.apk"); @@ -998,7 +1037,7 @@ public void moduleWithDependency_extractDependency() throws Exception { ApkTargeting.getDefaultInstance(), apkFeature3)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1021,8 +1060,9 @@ public void moduleWithDependency_extractDependency() throws Exception { } } + @Theory @Test - public void diamondModuleDependenciesGraph() throws Exception { + public void diamondModuleDependenciesGraph(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("base-master.apk"); ZipPath apkFeature1 = ZipPath.create("feature1-master.apk"); ZipPath apkFeature2 = ZipPath.create("feature2-master.apk"); @@ -1063,7 +1103,7 @@ public void diamondModuleDependenciesGraph() throws Exception { ApkTargeting.getDefaultInstance(), apkFeature4)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1088,8 +1128,9 @@ public void diamondModuleDependenciesGraph() throws Exception { } } + @Theory @Test - public void installTimeModule_alwaysExtracted() throws Exception { + public void installTimeModule_alwaysExtracted(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("base-master.apk"); ZipPath apkFeature1 = ZipPath.create("feature1-master.apk"); ZipPath apkFeature2 = ZipPath.create("feature2-master.apk"); @@ -1118,7 +1159,7 @@ public void installTimeModule_alwaysExtracted() throws Exception { ApkTargeting.getDefaultInstance(), apkFeature2)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1146,8 +1187,9 @@ public void printHelp_doesNotCrash() { ExtractApksCommand.help(); } + @Theory @Test - public void extractInstant_withBaseOnly() throws Exception { + public void extractInstant_withBaseOnly(TocFormat tocFormat) throws Exception { ZipPath apkLBase = ZipPath.create("apkL-base.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() @@ -1161,7 +1203,7 @@ public void extractInstant_withBaseOnly() throws Exception { createInstantApkSet("base", ApkTargeting.getDefaultInstance(), apkLBase))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1180,8 +1222,9 @@ public void extractInstant_withBaseOnly() throws Exception { } } + @Theory @Test - public void extractInstant_withNoInstantModules() throws Exception { + public void extractInstant_withNoInstantModules(TocFormat tocFormat) throws Exception { ZipPath apkPreL = ZipPath.create("apkPreL.apk"); ZipPath apkLBase = ZipPath.create("apkL-base.apk"); ZipPath apkLFeature = ZipPath.create("apkL-feature.apk"); @@ -1211,7 +1254,7 @@ public void extractInstant_withNoInstantModules() throws Exception { createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkLOther)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1229,8 +1272,9 @@ public void extractInstant_withNoInstantModules() throws Exception { assertThat(exception).hasMessageThat().contains("No compatible APKs found for the device"); } + @Theory @Test - public void extractApks_aboveMaxSdk_throws() throws Exception { + public void extractApks_aboveMaxSdk_throws(TocFormat tocFormat) throws Exception { BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() .setBundletool( @@ -1247,7 +1291,7 @@ public void extractApks_aboveMaxSdk_throws() throws Exception { ApkTargeting.getDefaultInstance(), ZipPath.create("base-master.apk"))))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(26); @@ -1265,8 +1309,9 @@ public void extractApks_aboveMaxSdk_throws() throws Exception { assertThat(exception).hasMessageThat().contains("No compatible APKs found for the device"); } + @Theory @Test - public void extractInstant_withModulesFlag() throws Exception { + public void extractInstant_withModulesFlag(TocFormat tocFormat) throws Exception { ZipPath apkPreL = ZipPath.create("apkPreL.apk"); ZipPath apkLBase = ZipPath.create("apkL-base.apk"); ZipPath apkLFeature = ZipPath.create("apkL-feature.apk"); @@ -1290,7 +1335,7 @@ public void extractInstant_withModulesFlag() throws Exception { createInstantApkSet("other", ApkTargeting.getDefaultInstance(), apkLOther))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1311,8 +1356,9 @@ public void extractInstant_withModulesFlag() throws Exception { } } + @Theory @Test - public void extractInstant_withBaseAndSingleInstantModule() throws Exception { + public void extractInstant_withBaseAndSingleInstantModule(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("apkL-base.apk"); ZipPath apkInstant = ZipPath.create("apkL-instant.apk"); ZipPath apkOther = ZipPath.create("apkL-other.apk"); @@ -1336,7 +1382,7 @@ public void extractInstant_withBaseAndSingleInstantModule() throws Exception { createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkOther)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1356,8 +1402,9 @@ public void extractInstant_withBaseAndSingleInstantModule() throws Exception { } } + @Theory @Test - public void extractInstant_withMultipleInstantModule() throws Exception { + public void extractInstant_withMultipleInstantModule(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("apkL-base.apk"); ZipPath apkInstant = ZipPath.create("apkL-instant.apk"); ZipPath apkInstant2 = ZipPath.create("apkL-instant2.apk"); @@ -1375,7 +1422,7 @@ public void extractInstant_withMultipleInstantModule() throws Exception { createInstantApkSet("other", ApkTargeting.getDefaultInstance(), apkInstant2))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1415,8 +1462,9 @@ public void testExtractFromDirectoryNoTableOfContents_throws() throws Exception } + @Theory @Test - public void conditionalModule_deviceMatching() throws Exception { + public void conditionalModule_deviceMatching(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("apkL-base.apk"); ZipPath apkConditional = ZipPath.create("apkN-conditional-module.apk"); BuildApksResult tableOfContentsProto = @@ -1439,7 +1487,7 @@ public void conditionalModule_deviceMatching() throws Exception { ApkTargeting.getDefaultInstance(), apkConditional)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = mergeSpecs(deviceWithSdk(24), deviceFeatures("android.hardware.camera.ar")); @@ -1459,8 +1507,9 @@ public void conditionalModule_deviceMatching() throws Exception { } } + @Theory @Test - public void conditionalModule_deviceNotMatching() throws Exception { + public void conditionalModule_deviceNotMatching(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("apkL-base.apk"); ZipPath apkConditional = ZipPath.create("apkN-conditional-module.apk"); BuildApksResult tableOfContentsProto = @@ -1483,7 +1532,7 @@ public void conditionalModule_deviceNotMatching() throws Exception { ApkTargeting.getDefaultInstance(), apkConditional)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = mergeSpecs(deviceWithSdk(21)); @@ -1501,8 +1550,10 @@ public void conditionalModule_deviceNotMatching() throws Exception { } } + @Theory @Test - public void conditionalModule_deviceNotMatching_moduleInFlags() throws Exception { + public void conditionalModule_deviceNotMatching_moduleInFlags(TocFormat tocFormat) + throws Exception { ZipPath apkBase = ZipPath.create("apkL-base.apk"); ZipPath apkConditional = ZipPath.create("apkN-conditional-module.apk"); BuildApksResult tableOfContentsProto = @@ -1525,7 +1576,7 @@ public void conditionalModule_deviceNotMatching_moduleInFlags() throws Exception ApkTargeting.getDefaultInstance(), apkConditional)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = mergeSpecs(deviceWithSdk(21)); @@ -1546,8 +1597,9 @@ public void conditionalModule_deviceNotMatching_moduleInFlags() throws Exception } /** Ensures that --modules=_ALL_ extracts all modules. */ + @Theory @Test - public void shortcutToExtractAllModules() throws Exception { + public void shortcutToExtractAllModules(TocFormat tocFormat) throws Exception { ZipPath apkBase = ZipPath.create("base-master.apk"); ZipPath apkBaseXxhdpi = ZipPath.create("base-xxhdpi.apk"); ZipPath apkFeature = ZipPath.create("feature-master.apk"); @@ -1581,7 +1633,7 @@ public void shortcutToExtractAllModules() throws Exception { createApkDescription(apkAbiTargeting(ARM64_V8A), apkFeature2Arm64, false)))) .build(); Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -1607,8 +1659,9 @@ public void shortcutToExtractAllModules() throws Exception { } } + @Theory @Test - public void extractAssetModules() throws Exception { + public void extractAssetModules(TocFormat tocFormat) throws Exception { String installTimeModule1 = "installtime_assetmodule1"; String installTimeModule2 = "installtime_assetmodule2"; String onDemandModule = "ondemand_assetmodule"; @@ -1662,7 +1715,8 @@ public void extractAssetModules() throws Exception { splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDeviceWithLocales("en-US"); @@ -1686,8 +1740,9 @@ public void extractAssetModules() throws Exception { } } + @Theory @Test - public void extractAssetModules_allModules() throws Exception { + public void extractAssetModules_allModules(TocFormat tocFormat) throws Exception { String installTimeModule1 = "installtime_assetmodule1"; String installTimeModule2 = "installtime_assetmodule2"; String onDemandModule = "ondemand_assetmodule"; @@ -1741,7 +1796,8 @@ public void extractAssetModules_allModules() throws Exception { splitApkDescription(ApkTargeting.getDefaultInstance(), onDemandMasterApk))) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDeviceWithLocales("en-US"); @@ -1767,8 +1823,10 @@ public void extractAssetModules_allModules() throws Exception { } } + @Theory @Test - public void bundleWithDeviceTierTargeting_noDeviceTierSpecified_usesDefaults() throws Exception { + public void bundleWithDeviceTierTargeting_noDeviceTierSpecified_usesDefaults(TocFormat tocFormat) + throws Exception { ZipPath baseMasterApk = ZipPath.create("base-master.apk"); ZipPath baseLowApk = ZipPath.create("base-tier_0.apk"); ZipPath baseHighApk = ZipPath.create("base-tier_1.apk"); @@ -1801,7 +1859,8 @@ public void bundleWithDeviceTierTargeting_noDeviceTierSpecified_usesDefaults() t .setDefaultValue("1")) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDevice(); @@ -1821,9 +1880,10 @@ public void bundleWithDeviceTierTargeting_noDeviceTierSpecified_usesDefaults() t } } + @Theory @Test - public void bundleWithDeviceTierTargeting_noDeviceTierSpecifiedNorDefault_usesZeroAsDefault() - throws Exception { + public void bundleWithDeviceTierTargeting_noDeviceTierSpecifiedNorDefault_usesZeroAsDefault( + TocFormat tocFormat) throws Exception { ZipPath baseMasterApk = ZipPath.create("base-master.apk"); ZipPath baseLowApk = ZipPath.create("base-tier_0.apk"); ZipPath baseHighApk = ZipPath.create("base-tier_1.apk"); @@ -1852,7 +1912,8 @@ public void bundleWithDeviceTierTargeting_noDeviceTierSpecifiedNorDefault_usesZe baseHighApk)))) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDevice(); @@ -1872,8 +1933,10 @@ public void bundleWithDeviceTierTargeting_noDeviceTierSpecifiedNorDefault_usesZe } } + @Theory @Test - public void bundleWithDeviceTierTargeting_deviceTierSet_filtersByTier() throws Exception { + public void bundleWithDeviceTierTargeting_deviceTierSet_filtersByTier(TocFormat tocFormat) + throws Exception { ZipPath baseMasterApk = ZipPath.create("base-master.apk"); ZipPath baseLowApk = ZipPath.create("base-tier_0.apk"); ZipPath baseHighApk = ZipPath.create("base-tier_1.apk"); @@ -1930,7 +1993,8 @@ public void bundleWithDeviceTierTargeting_deviceTierSet_filtersByTier() throws E .setDefaultValue("0")) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDevice().toBuilder().setDeviceTier(Int32Value.of(1)).build(); @@ -1954,9 +2018,10 @@ public void bundleWithDeviceTierTargeting_deviceTierSet_filtersByTier() throws E } } + @Theory @Test - public void bundleWithCountrySetTargeting_noCountrySetSpecifiedNorDefault_usesFallback() - throws Exception { + public void bundleWithCountrySetTargeting_noCountrySetSpecifiedNorDefault_usesFallback( + TocFormat tocFormat) throws Exception { ZipPath baseMasterApk = ZipPath.create("base-master.apk"); ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); @@ -1993,7 +2058,8 @@ public void bundleWithCountrySetTargeting_noCountrySetSpecifiedNorDefault_usesFa .addDefaultTargetingValue( DefaultTargetingValue.newBuilder().setDimension(Value.COUNTRY_SET)) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDevice(); ImmutableList matchedApks = @@ -2011,8 +2077,10 @@ public void bundleWithCountrySetTargeting_noCountrySetSpecifiedNorDefault_usesFa } } + @Theory @Test - public void bundleWithCountrySetTargeting_noCountrySetSpecified_usesDefaults() throws Exception { + public void bundleWithCountrySetTargeting_noCountrySetSpecified_usesDefaults(TocFormat tocFormat) + throws Exception { ZipPath baseMasterApk = ZipPath.create("base-master.apk"); ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); @@ -2051,7 +2119,8 @@ public void bundleWithCountrySetTargeting_noCountrySetSpecified_usesDefaults() t .setDimension(Value.COUNTRY_SET) .setDefaultValue("latam")) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDevice(); ImmutableList matchedApks = @@ -2069,9 +2138,10 @@ public void bundleWithCountrySetTargeting_noCountrySetSpecified_usesDefaults() t } } + @Theory @Test - public void bundleWithCountrySetTargeting_countrySetSpecified_filterByCountrySet() - throws Exception { + public void bundleWithCountrySetTargeting_countrySetSpecified_filterByCountrySet( + TocFormat tocFormat) throws Exception { ZipPath baseMasterApk = ZipPath.create("base-master.apk"); ZipPath baseRestOfWorldApk = ZipPath.create("base-other_countries.apk"); ZipPath baseSeaApk = ZipPath.create("base-countries_sea.apk"); @@ -2140,7 +2210,8 @@ public void bundleWithCountrySetTargeting_countrySetSpecified_filterByCountrySet .addDefaultTargetingValue( DefaultTargetingValue.newBuilder().setDimension(Value.COUNTRY_SET)) .build(); - Path apksArchiveFile = createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = lDevice().toBuilder().setCountrySet(StringValue.of("latam")).build(); ImmutableList matchedApks = @@ -2162,8 +2233,10 @@ public void bundleWithCountrySetTargeting_countrySetSpecified_filterByCountrySet } } + @Theory @Test - public void incompleteApksFile_missingMatchedAbiSplit_throws() throws Exception { + public void incompleteApksFile_missingMatchedAbiSplit_throws(TocFormat tocFormat) + throws Exception { // Partial APK Set file where 'x86' split is included and 'x86_64' split is not included because // device spec sent to 'build-apks' command doesn't support it. // Next, device spec that should be matched to 'x86_64' split is provided to 'extract-apks' @@ -2185,7 +2258,8 @@ public void incompleteApksFile_missingMatchedAbiSplit_throws() throws Exception ZipPath.create("base-x86.apk"))))) .build(); - Path apksArchiveFile = createApksArchiveFile(tableOfContent, tmpDir.resolve("bundle.apks")); + Path apksArchiveFile = + createApksArchiveFile(tableOfContent, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = mergeSpecs(deviceWithSdk(21), abis("x86_64", "x86")); @@ -2209,7 +2283,8 @@ public void incompleteApksFile_missingMatchedAbiSplit_throws() throws Exception @Test @Theory public void extractApks_producesOutputMetadata( - @FromDataPoints("localTestingEnabled") boolean localTestingEnabled) throws Exception { + @FromDataPoints("localTestingEnabled") boolean localTestingEnabled, TocFormat tocFormat) + throws Exception { String onDemandModule = "ondemand_assetmodule"; ZipPath apkBase = ZipPath.create("base-master.apk"); ZipPath apkFeature1 = ZipPath.create("feature1-master.apk"); @@ -2259,7 +2334,7 @@ public void extractApks_producesOutputMetadata( .build(); } Path apksArchiveFile = - createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks")); + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("bundle.apks"), tocFormat); DeviceSpec deviceSpec = deviceWithSdk(21); @@ -2323,8 +2398,10 @@ public void extractApks_producesOutputMetadata( .isEqualTo(expectedResult); } + @Theory @Test - public void extractApks_appWithRuntimeSdkVariant_noSdkRuntimeInSpec() throws Exception { + public void extractApks_appWithRuntimeSdkVariant_noSdkRuntimeInSpec(TocFormat tocFormat) + throws Exception { String withSdkRuntimeApk = "with_sdk_runtime.apk"; String withoutSdkRuntimeApk = "without_sdk_runtime.apk"; Variant variantWithSdkRuntime = @@ -2372,7 +2449,8 @@ public void extractApks_appWithRuntimeSdkVariant_noSdkRuntimeInSpec() throws Exc .addVariant(variantWithoutSdkRuntime) .setBundletool(Bundletool.newBuilder().setVersion("1.10.1")) .build(); - Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("app.apks")); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("app.apks"), tocFormat); Path deviceSpecFile = createDeviceSpecFile(deviceWithSdk(34), tmpDir.resolve("device.json")); ExtractApksCommand command = ExtractApksCommand.fromFlags( @@ -2384,8 +2462,10 @@ public void extractApks_appWithRuntimeSdkVariant_noSdkRuntimeInSpec() throws Exc .containsExactly(withSdkRuntimeApk); } + @Theory @Test - public void extractApks_appWithRuntimeSdkVariant_withSdkRuntimeInSpec() throws Exception { + public void extractApks_appWithRuntimeSdkVariant_withSdkRuntimeInSpec(TocFormat tocFormat) + throws Exception { String withSdkRuntimeApk = "with_sdk_runtime.apk"; String withoutSdkRuntimeApk = "without_sdk_runtime.apk"; Variant variantWithSdkRuntime = @@ -2433,7 +2513,8 @@ public void extractApks_appWithRuntimeSdkVariant_withSdkRuntimeInSpec() throws E .addVariant(variantWithoutSdkRuntime) .setBundletool(Bundletool.newBuilder().setVersion("1.10.1")) .build(); - Path apksArchiveFile = createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("app.apks")); + Path apksArchiveFile = + createApksArchiveFile(tableOfContentsProto, tmpDir.resolve("app.apks"), tocFormat); Path deviceSpecFile = createDeviceSpecFile( mergeSpecs(deviceWithSdk(34), sdkRuntimeSupported(false)), @@ -2530,12 +2611,13 @@ private static ExtractApksResult parseExtractApksResult(Path file) throws Except return builder.build(); } - private Path createApks(BuildApksResult buildApksResult, boolean apksInDirectory) + private Path createApks( + BuildApksResult buildApksResult, boolean apksInDirectory, TocFormat tocFormat) throws Exception { if (apksInDirectory) { - return createApksDirectory(buildApksResult, tmpDir); + return createApksDirectory(buildApksResult, tmpDir, tocFormat); } else { - return createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks")); + return createApksArchiveFile(buildApksResult, tmpDir.resolve("bundle.apks"), tocFormat); } } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java index fabf6add..8ca49d29 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/InstallMultiApksCommandTest.java @@ -85,6 +85,11 @@ public class InstallMultiApksCommandTest { private static final String DEVICE_ID = "id1"; private static final String PKG_NAME_1 = "com.example.a"; private static final String PKG_NAME_2 = "com.example.b"; + private static final String NONUPDATABLE_PKG_NAME_1 = "com.google.android.permissionconfig"; + private static final String NONUPDATABLE_PKG_NAME_2 = "com.google.android.ext.service"; + private static final String NONUPDATABLE_PKG_NAME_3 = "com.google.android.permissioncontroller"; + private static final String NONUPDATABLE_PKG_NAME_4 = "com.google.android.extservice"; + private static final String NONUPDATABLE_PKG_NAME_5 = "com.google.android.permission"; @Rule public TemporaryFolder tmp = new TemporaryFolder(); @@ -560,6 +565,73 @@ public void execute_updateOnly_apex() throws Exception { assertAdbCommandExecuted(); } + @Test + public void execute_installUpdatablePackageOnly() throws Exception { + + BuildApksResult tableOfContents1 = fakeTableOfContents(NONUPDATABLE_PKG_NAME_1); + Path apksPath1 = createApksArchiveFile(tableOfContents1, tmpDir.resolve("package1.apks")); + BuildApksResult tableOfContents2 = fakeTableOfContents(NONUPDATABLE_PKG_NAME_2); + Path apksPath2 = createApksArchiveFile(tableOfContents2, tmpDir.resolve("package2.apks")); + BuildApksResult tableOfContents3 = fakeTableOfContents(NONUPDATABLE_PKG_NAME_3); + Path apksPath3 = createApksArchiveFile(tableOfContents3, tmpDir.resolve("package3.apks")); + BuildApksResult tableOfContents4 = fakeTableOfContents(NONUPDATABLE_PKG_NAME_4); + Path apksPath4 = createApksArchiveFile(tableOfContents4, tmpDir.resolve("package4.apks")); + BuildApksResult tableOfContents5 = fakeTableOfContents(NONUPDATABLE_PKG_NAME_5); + Path apksPath5 = createApksArchiveFile(tableOfContents5, tmpDir.resolve("package5.apks")); + + InstallMultiApksCommand command = + InstallMultiApksCommand.builder() + .setAdbServer(fakeServerOneDevice(device)) + .setDeviceId(DEVICE_ID) + .setAdbPath(adbPath) + .setApksArchivePaths( + ImmutableList.of(apksPath1, apksPath2, apksPath3, apksPath4, apksPath5)) + .setAapt2Command( + createFakeAapt2Command( + ImmutableMap.of( + NONUPDATABLE_PKG_NAME_1, + 1L, + NONUPDATABLE_PKG_NAME_2, + 1L, + NONUPDATABLE_PKG_NAME_3, + 1L, + NONUPDATABLE_PKG_NAME_4, + 1L, + NONUPDATABLE_PKG_NAME_5, + 1L))) + .setAdbCommand( + // EXPECT three packages to be installed. + createFakeAdbCommand( + ImmutableListMultimap.builder() + .putAll(expectedInstallApks(NONUPDATABLE_PKG_NAME_1, tableOfContents1)) + .putAll(expectedInstallApks(NONUPDATABLE_PKG_NAME_4, tableOfContents4)) + .putAll(expectedInstallApks(NONUPDATABLE_PKG_NAME_5, tableOfContents5)) + .build(), + /* expectedStaged= */ false, + /* expectedEnableRollback= */ false, + Optional.of(DEVICE_ID))) + .build(); + + // EXPECT to filter out the non-updatable packages which have updatable versions available. + device.injectShellCommandOutput( + "pm list packages --show-versioncode", + () -> + String.format( + "package:%s versionCode:1\n" + + "package:%s versionCode:1\n" + + "package:%s versionCode:1\n" + + "package:%s versionCode:1\n" + + "package:%s versionCode:1", + NONUPDATABLE_PKG_NAME_1, + NONUPDATABLE_PKG_NAME_2, + NONUPDATABLE_PKG_NAME_3, + NONUPDATABLE_PKG_NAME_4, + NONUPDATABLE_PKG_NAME_5)); + device.injectShellCommandOutput("pm list packages --apex-only --show-versioncode", () -> ""); + command.execute(); + assertAdbCommandExecuted(); + } + @Test public void execute_gracefulExitIfNoPackagesFound() throws Exception { // GIVEN a zip file containing fake .apks files for multiple packages. diff --git a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java index 71e05afc..45955ff6 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/AndroidManifestTest.java @@ -50,6 +50,8 @@ 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.androidManifestForMlModule; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.createIntentFilterForMainActivity; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withActivityAlias; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withCustomThemeActivity; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFastFollowDelivery; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFusingAttribute; @@ -123,6 +125,27 @@ public void getApplicationDebuggable_absent() { assertThat(androidManifest.getEffectiveApplicationDebuggable()).isFalse(); } + @Test + public void hasMainActivity_definedAsActivityAlias_returnTrue() { + AndroidManifest androidManifest = + AndroidManifest.create( + androidManifest( + "com.test.app", + withActivityAlias( + "com.test.app.MainActivity", + activity -> + activity + .addChildElement( + createIntentFilterForMainActivity( + "android.intent.category.LAUNCHER")) + .addChildElement( + createIntentFilterForMainActivity( + "android.intent.category.LEANBACK_LAUNCHER"))))); + + assertThat(androidManifest.hasMainTvActivity()).isTrue(); + assertThat(androidManifest.hasMainActivity()).isTrue(); + } + @Test public void getApplicationDebuggable_presentFalse() { AndroidManifest androidManifest = diff --git a/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java b/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java index 30f76df8..6394c53b 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/BundleModuleTest.java @@ -455,6 +455,7 @@ public void getModuleMetadataForSdkDependencyModule_sdkModuleMetadataSet() { .setMinor(sdkMinorVersion) .setPatch(sdkPatchVersion)) .build()) + .setResourcesPackageId(2) .build(); assertThat(bundleModule.getModuleMetadata().getSdkModuleMetadata()) @@ -466,6 +467,7 @@ public void getModuleMetadataForSdkDependencyModule_sdkModuleMetadataSet() { .setMajor(sdkMajorVersion) .setMinor(sdkMinorVersion) .setPatch(sdkPatchVersion)) + .setResourcesPackageId(2) .build()); } @@ -571,6 +573,7 @@ public void getModuleType_sdkModule() { .setAndroidManifestProto(androidManifestForSdkModule("com.test.app")) .setSdkModulesConfig( SdkModulesConfig.newBuilder().setSdkPackageName("com.test.sdk").build()) + .setResourcesPackageId(2) .build(); assertThat(bundleModule.getModuleType()).isEqualTo(ModuleType.SDK_DEPENDENCY_MODULE); @@ -578,12 +581,34 @@ public void getModuleType_sdkModule() { @Test public void missingSdkModulesConfigForSdkDependencyModule_throws() { - assertThrows( - IllegalStateException.class, - () -> - createMinimalModuleBuilder() - .setAndroidManifestProto(androidManifestForSdkModule("com.test.app")) - .build()); + Throwable e = + assertThrows( + IllegalStateException.class, + () -> + createMinimalModuleBuilder() + .setAndroidManifestProto(androidManifestForSdkModule("com.test.app")) + .setResourcesPackageId(2) + .build()); + assertThat(e) + .hasMessageThat() + .contains( + "BundleModule of type SDK_DEPENDENCY_MODULE can not have empty SdkModulesConfig."); + } + + @Test + public void missingResourcesPackageIdForSdkDependencyModule_throws() { + Throwable e = + assertThrows( + IllegalStateException.class, + () -> + createMinimalModuleBuilder() + .setAndroidManifestProto(androidManifestForSdkModule("com.test.app")) + .setSdkModulesConfig(SdkModulesConfig.getDefaultInstance()) + .build()); + assertThat(e) + .hasMessageThat() + .contains( + "BundleModule of type SDK_DEPENDENCY_MODULE can not have empty ResourcesPackageId."); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java b/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java index 6f8380de..352e7791 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/utils/ResultUtilsTest.java @@ -34,6 +34,8 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Commands.ApkDescription; import com.android.bundle.Commands.ApkSet; @@ -48,20 +50,28 @@ import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.SdkVersion; import com.android.bundle.Targeting.VariantTargeting; +import com.android.tools.build.bundletool.io.ZipBuilder; import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.testing.ApksArchiveHelpers; +import com.android.tools.build.bundletool.testing.ApksArchiveHelpers.TocFormat; import com.google.common.collect.ImmutableSet; +import com.google.protobuf.util.JsonFormat; import java.nio.file.Path; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(Theories.class) public class ResultUtilsTest { + private static final BuildApksResult DEFAULT_BUILD_APKS_RESULT = + ResultUtils.applyDefaultValues(BuildApksResult.getDefaultInstance()); + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private Path tmpDir; @@ -77,7 +87,7 @@ public void emptyBuildApksResult_readTableOfContents() throws Exception { BuildApksResult buildApksResult = readTableOfContents(apksArchiveFile); - assertThat(buildApksResult).isEqualToDefaultInstance(); + assertThat(buildApksResult).isEqualTo(DEFAULT_BUILD_APKS_RESULT); } @Test @@ -88,16 +98,19 @@ public void emptyBuildSdkApksResult_readTableOfContents() throws Exception { BuildApksResult buildApksResult = readTableOfContents(sdkApksArchiveFile); - assertThat(buildApksResult).isEqualToDefaultInstance(); + assertThat(buildApksResult).isEqualTo(DEFAULT_BUILD_APKS_RESULT); } + @Theory @Test - public void emptyBuildApksResult_inDirectory_readTableOfContents() throws Exception { - Path apksDirectory = createApksDirectory(BuildApksResult.getDefaultInstance(), tmpDir); + public void emptyBuildApksResult_inDirectory_readTableOfContents(TocFormat tocFormat) + throws Exception { + Path apksDirectory = + createApksDirectory(BuildApksResult.getDefaultInstance(), tmpDir, tocFormat); BuildApksResult buildApksResult = readTableOfContents(apksDirectory); - assertThat(buildApksResult).isEqualToDefaultInstance(); + assertThat(buildApksResult).isEqualTo(DEFAULT_BUILD_APKS_RESULT); } @Test @@ -117,7 +130,8 @@ public void buildApksResult_readTableOfContents() throws Exception { BuildApksResult buildApksResult = readTableOfContents(apksArchiveFile); - assertThat(buildApksResult).isEqualTo(tableOfContentsProto); + assertThat(buildApksResult) + .isEqualTo(tableOfContentsProto.toBuilder().mergeFrom(DEFAULT_BUILD_APKS_RESULT).build()); } @Test @@ -155,8 +169,10 @@ public void buildSdkApksResult_readTableOfContents() throws Exception { .build()); } + @Theory @Test - public void buildApksResult_inDirectory_readTableOfContents() throws Exception { + public void buildApksResult_inDirectory_readTableOfContents(TocFormat tocFormat) + throws Exception { ZipPath apkLBase = ZipPath.create("apkL-base.apk"); BuildApksResult tableOfContentsProto = BuildApksResult.newBuilder() @@ -167,11 +183,12 @@ public void buildApksResult_inDirectory_readTableOfContents() throws Exception { "base", createMasterApkDescription(ApkTargeting.getDefaultInstance(), apkLBase)))) .build(); - - Path apksDirectory = createApksDirectory(tableOfContentsProto, tmpDir); + Path apksDirectory = createApksDirectory(tableOfContentsProto, tmpDir, tocFormat); BuildApksResult buildApksResult = readTableOfContents(apksDirectory); - assertThat(buildApksResult).isEqualTo(tableOfContentsProto); + + assertThat(buildApksResult) + .isEqualTo(tableOfContentsProto.toBuilder().mergeFrom(DEFAULT_BUILD_APKS_RESULT).build()); } @Test @@ -329,6 +346,33 @@ public void getAllTargetedLanguages() { assertThat(langs).containsExactly("pl", "en", "ru", "fr"); } + @Test + public void jsonToc_withNobundletoolVersion_defaultApplied() throws Exception { + Path apksDirectory = + createApksDirectory(BuildApksResult.getDefaultInstance(), tmpDir, TocFormat.JSON); + + BuildApksResult buildApksResult = readTableOfContents(apksDirectory); + + assertThat(buildApksResult.getBundletool().getVersion()) + .isEqualTo(BundleToolVersion.getCurrentVersion().toString()); + } + + @Test + public void jsonTocAndProtoToc_throws() throws Exception { + Path apksPath = tmpDir.resolve("file.apks"); + ZipBuilder archiveBuilder = new ZipBuilder(); + archiveBuilder.addFileWithProtoContent( + ZipPath.create("toc.pb"), BuildApksResult.getDefaultInstance()); + archiveBuilder.addFileWithContent( + ZipPath.create("toc.json"), + JsonFormat.printer().print(BuildApksResult.getDefaultInstance()).getBytes(UTF_8)); + archiveBuilder.writeTo(apksPath); + + assertThat(assertThrows(IllegalStateException.class, () -> readTableOfContents(apksPath))) + .hasMessageThat() + .contains("Apks archive cannot have both toc.pb and toc.json"); + } + private Variant createInstantVariant() { ZipPath apkLBase = ZipPath.create("instant/apkL-base.apk"); ZipPath apkLFeature = ZipPath.create("instant/apkL-feature.apk"); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java index 7e864f70..ed47f8a2 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/ScreenDensityResourcesSplitterTest.java @@ -131,6 +131,7 @@ public void noDensityResources_noDensitySplits() throws Exception { .addFile("res/drawable/test.jpg") .setResourceTable( resourceTable( + StringPool.newBuilder().setData(ByteString.copyFrom(new byte[] {'x'})).build(), pkg( USER_PACKAGE_OFFSET, "com.test.app", @@ -156,6 +157,44 @@ public void noDensityResources_noDensitySplits() throws Exception { assertThat(baseSplit.findEntry("res/drawable/test.jpg")).isPresent(); } + @Test + public void noDensityResources_noDensitySplits_preFixSkipGeneratingEmptyDensitySplits() { + BundleModule testModule = + new BundleModuleBuilder("testModule") + .addFile("res/drawable/test.jpg") + .setResourceTable( + resourceTable( + pkg( + USER_PACKAGE_OFFSET, + "com.test.app", + type( + 0x01, + "drawable", + entry( + 0x01, + "test", + fileReference( + "res/drawable/test.jpg", + Configuration.getDefaultInstance())))))) + .setManifest(androidManifest("com.test.app")) + .build(); + ModuleSplit resourcesModule = ModuleSplit.forResources(testModule); + ScreenDensityResourcesSplitter splitter = + new ScreenDensityResourcesSplitter( + // Fix was introduced in 1.15.1 + Version.of("1.15.0"), + NO_RESOURCES_PINNED_TO_MASTER, + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ false); + + ImmutableCollection splits = splitter.split(resourcesModule); + + assertThat(splits).hasSize(1); + ModuleSplit baseSplit = splits.iterator().next(); + assertThat(baseSplit.getResourceTable().get()).containsResource("com.test.app:drawable/test"); + assertThat(baseSplit.findEntry("res/drawable/test.jpg")).isPresent(); + } + @Test public void allSplitsPresentWithResourceTable() throws Exception { BundleModule testModule = @@ -280,6 +319,41 @@ public void mipmapsNotIncludedInConfigSplits() throws Exception { @Test public void preservesSourcePool() throws Exception { + StringPool sourcePool = + StringPool.newBuilder().setData(ByteString.copyFrom(new byte[] {'x'})).build(); + ResourceTable table = + new ResourceTableBuilder() + .addPackage("com.test.app") + .addDrawableResourceForMultipleDensities( + "image", + ImmutableMap.of( + MDPI_VALUE, + "res/drawable-mdpi/image.jpg", + XHDPI_VALUE, + "res/drawable-xhdpi/image.jpg")) + .build() + .toBuilder() + .setSourcePool(sourcePool) + .build(); + BundleModule testModule = + new BundleModuleBuilder("testModule") + .addFile("res/drawable-mdpi/test.jpg") + .setResourceTable(table) + .setManifest(androidManifest("com.test.app")) + .build(); + + ImmutableCollection densitySplits = + splitter.split(ModuleSplit.forResources(testModule)); + assertThat(densitySplits).hasSize(DEFAULT_DENSITY_BUCKETS.size() + 1); + + for (ModuleSplit densitySplit : densitySplits) { + assertThat(densitySplit.getResourceTable()).isPresent(); + assertThat(densitySplit.getResourceTable().get().getSourcePool()).isEqualTo(sourcePool); + } + } + + @Test + public void generateAllDensitySplitsBeforeDensitySplitGenerationFix() { StringPool sourcePool = StringPool.newBuilder().setData(ByteString.copyFrom(new byte[] {'x'})).build(); ResourceTable table = @@ -297,6 +371,13 @@ public void preservesSourcePool() throws Exception { .setResourceTable(table) .setManifest(androidManifest("com.test.app")) .build(); + ScreenDensityResourcesSplitter splitter = + new ScreenDensityResourcesSplitter( + // Fix was introduced in 1.15.1 + Version.of("1.15.0"), + NO_RESOURCES_PINNED_TO_MASTER, + NO_LOW_DENSITY_CONFIG_PINNED_TO_MASTER, + /* pinLowestBucketOfStylesToMaster= */ false); ImmutableCollection densitySplits = splitter.split(ModuleSplit.forResources(testModule)); diff --git a/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java b/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java index 09d1299c..e9bf5bf9 100644 --- a/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/splitters/SplitApksGeneratorTest.java @@ -535,6 +535,7 @@ public void appBundleWithSdkDependencyModule_sdkModuleIncludedInNonSdkRuntimeVar new BundleModuleBuilder("comTestSdk") .setModuleType(ModuleType.SDK_DEPENDENCY_MODULE) .setSdkModulesConfig(SdkModulesConfig.getDefaultInstance()) + .setResourcesPackageId(2) .setManifest(androidManifest("com.test.sdk")) .build()) .build(); @@ -614,11 +615,13 @@ public void appBundleWithSdkDependencyModuleAndDensityTargeting_noDensitySplitsF .setCertificateDigest("AA:BB:CC") .setResourcesPackageId(2)) .build()) + .setResourcesPackageId(2) .build()) .addModule( - new BundleModuleBuilder("comTestSdk") + new BundleModuleBuilder("comtestsdk") .setModuleType(ModuleType.SDK_DEPENDENCY_MODULE) .setSdkModulesConfig(SdkModulesConfig.getDefaultInstance()) + .setResourcesPackageId(2) .setManifest(androidManifest("com.test.sdk")) .setResourceTable(sdkResourceTable) .build()) @@ -646,10 +649,10 @@ public void appBundleWithSdkDependencyModuleAndDensityTargeting_noDensitySplitsF .map(ModuleSplit::getModuleName) .map(BundleModuleName::getName) .distinct()) - .containsExactly("base", "comTestSdk"); + .containsExactly("base", "comtestsdk"); ImmutableSet sdkModuleSplits = moduleSplitMap.get(lPlusVariantTargeting()).stream() - .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("comTestSdk")) + .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("comtestsdk")) .collect(toImmutableSet()); // Only main split for SDK dependency module - no config splits. assertThat(sdkModuleSplits).hasSize(1); diff --git a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java index 827d0742..95538c19 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/ApksArchiveHelpers.java @@ -22,6 +22,7 @@ import static com.android.tools.build.bundletool.testing.TargetingUtils.variantMultiAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.variantSdkTargeting; import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.nio.charset.StandardCharsets.UTF_8; import com.android.bundle.Commands.ApexApkMetadata; import com.android.bundle.Commands.ApkDescription; @@ -46,6 +47,7 @@ import com.android.tools.build.bundletool.model.version.BundleToolVersion; import com.android.tools.build.bundletool.model.version.Version; import com.google.common.collect.ImmutableList; +import com.google.protobuf.util.JsonFormat; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -56,15 +58,38 @@ public final class ApksArchiveHelpers { private static final byte[] TEST_BYTES = new byte[100]; + /** The format of the table of content when serialized to disk. */ + public enum TocFormat { + PROTO, + JSON + } + /** Create an app APK set and serialize it to the provided path. */ public static Path createApksArchiveFile(BuildApksResult result, Path location) throws Exception { + return createApksArchiveFile(result, location, TocFormat.PROTO); + } + + /** + * Create an app APK set and serialize it to the provided path. The toc file will be serialized in + * the given tocFormat + */ + public static Path createApksArchiveFile( + BuildApksResult result, Path location, TocFormat tocFormat) throws Exception { ZipBuilder archiveBuilder = new ZipBuilder(); apkDescriptionStream(result) .forEach( apkDesc -> archiveBuilder.addFileWithContent(ZipPath.create(apkDesc.getPath()), TEST_BYTES)); - archiveBuilder.addFileWithProtoContent(ZipPath.create("toc.pb"), result); + switch (tocFormat) { + case PROTO: + archiveBuilder.addFileWithProtoContent(ZipPath.create("toc.pb"), result); + break; + case JSON: + archiveBuilder.addFileWithContent( + ZipPath.create("toc.json"), JsonFormat.printer().print(result).getBytes(UTF_8)); + break; + } return archiveBuilder.writeTo(location); } @@ -86,6 +111,11 @@ public static Path createSdkApksArchiveFile(BuildSdkApksResult result, Path loca } public static Path createApksDirectory(BuildApksResult result, Path location) throws Exception { + return createApksDirectory(result, location, TocFormat.PROTO); + } + + public static Path createApksDirectory(BuildApksResult result, Path location, TocFormat tocFormat) + throws Exception { ImmutableList apkDescriptions = apkDescriptionStream(result).collect(toImmutableList()); @@ -94,7 +124,14 @@ public static Path createApksDirectory(BuildApksResult result, Path location) th Files.createDirectories(apkPath.getParent()); Files.write(apkPath, TEST_BYTES); } - Files.write(location.resolve("toc.pb"), result.toByteArray()); + switch (tocFormat) { + case PROTO: + Files.write(location.resolve("toc.pb"), result.toByteArray()); + break; + case JSON: + Files.writeString(location.resolve("toc.json"), JsonFormat.printer().print(result)); + break; + } return location; } diff --git a/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java b/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java index eda39b17..8fd7d5e6 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/BundleModuleBuilder.java @@ -57,6 +57,8 @@ public class BundleModuleBuilder { private Optional sdkModulesConfigOptional = Optional.empty(); + private Optional resourcesPackageIdOptional = Optional.empty(); + public BundleModuleBuilder(String moduleName) { checkNotNull(moduleName); this.moduleName = BundleModuleName.create(moduleName); @@ -162,6 +164,12 @@ public BundleModuleBuilder setModuleType(ModuleType moduleType) { return this; } + @CanIgnoreReturnValue + public BundleModuleBuilder setResourcesPackageId(int resourcesPackageId) { + this.resourcesPackageIdOptional = Optional.of(resourcesPackageId); + return this; + } + public BundleModule build() { if (androidManifest != null) { XmlProtoNodeBuilder manifestBuilder = new XmlProtoNode(androidManifest).toBuilder(); @@ -188,6 +196,7 @@ public BundleModule build() { .setBundletoolVersion(BundleToolVersion.getCurrentVersion()); moduleTypeOptional.ifPresent(bundleModuleBuilder::setModuleType); sdkModulesConfigOptional.ifPresent(bundleModuleBuilder::setSdkModulesConfig); + resourcesPackageIdOptional.ifPresent(bundleModuleBuilder::setResourcesPackageId); if (!bundleConfig.getBundletool().getVersion().isEmpty()) { bundleModuleBuilder.setBundletoolVersion( 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 9eda93a3..dfcecc9b 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 @@ -975,24 +975,47 @@ public static ManifestMutator withActivity( .setValueAsString(name)))); } + public static ManifestMutator withActivityAlias( + String name, Function modifier) { + return manifestElement -> + manifestElement + .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) + .addChildElement( + modifier.apply( + XmlProtoElementBuilder.create(ACTIVITY_ALIAS_ELEMENT_NAME) + .addAttribute( + XmlProtoAttributeBuilder.createAndroidAttribute( + NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) + .setValueAsString(name)))); + } + private static ManifestMutator withActivity(String activityName, String categoryName) { return withActivity( activityName, - activity -> - activity.addChildElement( - XmlProtoElementBuilder.create("intent-filter") - .addChildElement( - XmlProtoElementBuilder.create("action") - .addAttribute( - XmlProtoAttributeBuilder.createAndroidAttribute( - NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) - .setValueAsString("android.intent.action.MAIN"))) - .addChildElement( - XmlProtoElementBuilder.create("category") - .addAttribute( - XmlProtoAttributeBuilder.createAndroidAttribute( - NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) - .setValueAsString(categoryName))))); + activity -> activity.addChildElement(createIntentFilterForMainActivity(categoryName))); + } + + public static XmlProtoElementBuilder createIntentFilter( + Function modifier) { + return modifier.apply(XmlProtoElementBuilder.create("intent-filter")); + } + + public static XmlProtoElementBuilder createIntentFilterForMainActivity(String categoryName) { + return createIntentFilter( + intentFilter -> + intentFilter + .addChildElement( + XmlProtoElementBuilder.create("action") + .addAttribute( + XmlProtoAttributeBuilder.createAndroidAttribute( + NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) + .setValueAsString("android.intent.action.MAIN"))) + .addChildElement( + XmlProtoElementBuilder.create("category") + .addAttribute( + XmlProtoAttributeBuilder.createAndroidAttribute( + NAME_ATTRIBUTE_NAME, NAME_RESOURCE_ID) + .setValueAsString(categoryName)))); } public static ManifestMutator withCustomThemeActivity(String name, int themeRefId) { diff --git a/src/test/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidatorTest.java index 90765b4f..31c74915 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/DeclarativeWatchFaceBundleValidatorTest.java @@ -35,17 +35,24 @@ import com.android.tools.build.bundletool.testing.AppBundleBuilder; import com.android.tools.build.bundletool.testing.BundleModuleBuilder; import com.android.tools.build.bundletool.testing.ManifestProtoUtils.ManifestMutator; +import java.nio.file.Path; import java.util.AbstractMap.SimpleEntry; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.stream.Stream; +import java.util.zip.ZipFile; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class DeclarativeWatchFaceBundleValidatorTest { + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + private static final String DWF_BUNDLE_PACKAGE = "com.sample.dwf"; private static final int MIN_DWF_SDK_VERSION = 33; private static final int EMBEDDED_RUNTIME_SDK_VERSION = 30; @@ -83,6 +90,13 @@ public void validDwfWithEmbeddedRuntime() { new DeclarativeWatchFaceBundleValidator().validateBundle(appBundle); } + @Test + public void validDwfAabWithEmbeddedRuntime() throws Exception { + Path wfAabPath = TestData.copyToTempDir(tmp, "testdata/bundle/watch-face-from-tool.aab"); + AppBundle appBundle = AppBundle.buildFromZip(new ZipFile(wfAabPath.toFile())); + new DeclarativeWatchFaceBundleValidator().validateBundle(appBundle); + } + @Test public void invalidDwf_notTargetingWatches() { AppBundle appBundle = @@ -291,22 +305,29 @@ public void invalidSimpleDwf_hasLibsInBase() { } @Test - public void invalidSimpleDwf_hasRootFilesInBase() { - AppBundle appBundle = - new AppBundleBuilder() - .addModule( - new BundleModuleBuilder("base") - .setManifest( - createDwfManifest( - withUsesFeatureElement( - AndroidManifest.USES_FEATURE_HARDWARE_WATCH_NAME))) - .addFile("/res/raw/watchface.xml") - .addFile("/root/some-file.txt") - .build()) - .build(); - - InvalidBundleException e = assertThrowsForBundle(appBundle); - assertThat(e).hasMessageThat().contains("cannot have any files in the root of the package"); + public void invalidSimpleDwf_hasCodeFilesInBase() { + List codeFileNames = Arrays.asList("classes.dex", "libs.so"); + + codeFileNames.forEach( + fileName -> { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + new BundleModuleBuilder("base") + .setManifest( + createDwfManifest( + withUsesFeatureElement( + AndroidManifest.USES_FEATURE_HARDWARE_WATCH_NAME))) + .addFile("/res/raw/watchface.xml") + .addFile("/root/" + fileName) + .build()) + .build(); + + InvalidBundleException e = assertThrowsForBundle(appBundle); + assertThat(e) + .hasMessageThat() + .contains("cannot have any compiled code in its root folder"); + }); } @Test diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/bundle/watch-face-from-tool.aab b/src/test/resources/com/android/tools/build/bundletool/testdata/bundle/watch-face-from-tool.aab new file mode 100644 index 00000000..1b551bd9 Binary files /dev/null and b/src/test/resources/com/android/tools/build/bundletool/testdata/bundle/watch-face-from-tool.aab differ diff --git a/src/test/resources/com/android/tools/build/bundletool/testdata/xml/opt-out.xml b/src/test/resources/com/android/tools/build/bundletool/testdata/xml/opt-out.xml new file mode 100644 index 00000000..cdf6c95e --- /dev/null +++ b/src/test/resources/com/android/tools/build/bundletool/testdata/xml/opt-out.xml @@ -0,0 +1,2 @@ + +optOut \ No newline at end of file