diff --git a/README.md b/README.md index e6ae3a2a..28fbf3be 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,4 @@ https://developer.android.com/studio/command-line/bundletool ## Releases -Latest release: [1.17.1](https://github.com/google/bundletool/releases) +Latest release: [1.17.2](https://github.com/google/bundletool/releases) diff --git a/gradle.properties b/gradle.properties index fe59b282..5bedf5cf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -release_version = 1.17.1 +release_version = 1.17.2 diff --git a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java index e89edee6..26e08263 100644 --- a/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java +++ b/src/main/java/com/android/tools/build/bundletool/BundleToolMain.java @@ -25,6 +25,7 @@ import com.android.tools.build.bundletool.commands.CheckTransparencyCommand; import com.android.tools.build.bundletool.commands.CommandHelp; import com.android.tools.build.bundletool.commands.DumpCommand; +import com.android.tools.build.bundletool.commands.DumpSdkBundleCommand; import com.android.tools.build.bundletool.commands.EvaluateDeviceTargetingConfigCommand; import com.android.tools.build.bundletool.commands.ExtractApksCommand; import com.android.tools.build.bundletool.commands.GetDeviceSpecCommand; @@ -47,7 +48,7 @@ * *

Consider running with -Dsun.zip.disableMemoryMapping when dealing with large bundles. */ -public class BundleToolMain { +public final class BundleToolMain { public static final String HELP_CMD = "help"; @@ -128,6 +129,9 @@ static void main(String[] args, Runtime runtime) { case DumpCommand.COMMAND_NAME: DumpCommand.fromFlags(flags).execute(); break; + case DumpSdkBundleCommand.COMMAND_NAME: + DumpSdkBundleCommand.fromFlags(flags).execute(); + break; case GetSizeCommand.COMMAND_NAME: GetSizeCommand.fromFlags(flags).execute(); break; @@ -259,4 +263,6 @@ public static void help(String commandName, Runtime runtime) { commandHelp.printDetails(System.out); } + + private BundleToolMain() {} } diff --git a/src/main/java/com/android/tools/build/bundletool/androidtools/DefaultCommandExecutor.java b/src/main/java/com/android/tools/build/bundletool/androidtools/DefaultCommandExecutor.java index 1ec4d792..7ff7b4a2 100644 --- a/src/main/java/com/android/tools/build/bundletool/androidtools/DefaultCommandExecutor.java +++ b/src/main/java/com/android/tools/build/bundletool/androidtools/DefaultCommandExecutor.java @@ -20,43 +20,57 @@ import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; import com.android.tools.build.bundletool.model.utils.files.BufferedIo; import com.google.common.collect.ImmutableList; -import com.google.common.io.CharStreams; +import com.google.common.io.LineReader; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; import java.io.UncheckedIOException; +import java.lang.ProcessBuilder.Redirect; +import java.util.ArrayList; +import java.util.List; /** Helper to execute native commands. */ public final class DefaultCommandExecutor implements CommandExecutor { @Override public void execute(ImmutableList command, CommandOptions options) { - executeImpl(command, options); + ImmutableList capturedOutput = executeImpl(command, options); + printOutput(capturedOutput, System.out); } @Override public ImmutableList executeAndCapture( ImmutableList command, CommandOptions options) { - return captureOutput(executeImpl(command, options)); + return executeImpl(command, options); } - private static Process executeImpl(ImmutableList command, CommandOptions options) { + private static ImmutableList executeImpl( + ImmutableList command, CommandOptions options) { try { - Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + Process process = + new ProcessBuilder(command) + .redirectOutput(Redirect.PIPE) + .redirectErrorStream(true) + .start(); + + OutputCapturer outputCapturer = OutputCapturer.startCapture(process.getInputStream()); + if (!process.waitFor(options.getTimeout().toMillis(), MILLISECONDS)) { - printOutput(process); + printOutput(outputCapturer.getOutput(/* interrupt= */ true), System.err); throw CommandExecutionException.builder() .withInternalMessage("Command timed out: %s", command) .build(); } if (process.exitValue() != 0) { - printOutput(process); + printOutput(outputCapturer.getOutput(/* interrupt= */ true), System.err); throw CommandExecutionException.builder() .withInternalMessage( "Command '%s' didn't terminate successfully (exit code: %d). Check the logs.", command, process.exitValue()) .build(); } - return process; + return outputCapturer.getOutput(/* interrupt= */ false); } catch (IOException | InterruptedException e) { throw CommandExecutionException.builder() .withInternalMessage("Error when executing command: %s", command) @@ -65,22 +79,48 @@ private static Process executeImpl(ImmutableList command, CommandOptions } } - private static ImmutableList captureOutput(Process process) { - try (BufferedReader outputReader = BufferedIo.reader(process.getInputStream())) { - return ImmutableList.copyOf(CharStreams.readLines(outputReader)); - } catch (IOException e) { - throw new UncheckedIOException(e); + static class OutputCapturer { + private final Thread thread; + private final List output; + private final InputStream stream; + + private OutputCapturer(Thread thread, List output, InputStream stream) { + this.thread = thread; + this.output = output; + this.stream = stream; } - } - private static void printOutput(Process process) { - try (BufferedReader outputReader = BufferedIo.reader(process.getInputStream())) { - String line; - while ((line = outputReader.readLine()) != null) { - System.err.println(line); + static OutputCapturer startCapture(InputStream stream) { + List output = new ArrayList<>(); + Thread thread = + new Thread( + () -> { + try (BufferedReader reader = BufferedIo.reader(stream)) { + LineReader lineReader = new LineReader(reader); + String line; + while ((line = lineReader.readLine()) != null) { + output.add(line); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + thread.start(); + return new OutputCapturer(thread, output, stream); + } + + ImmutableList getOutput(boolean interrupt) throws InterruptedException, IOException { + if (interrupt) { + stream.close(); } - } catch (IOException e) { - System.err.println("Error when printing output of command:" + e.getMessage()); + thread.join(); + return ImmutableList.copyOf(output); + } + } + + private static void printOutput(List output, PrintStream stream) { + for (String line : output) { + stream.println(line); } } } 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 980931e4..530d0d89 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 @@ -920,6 +920,7 @@ public Path execute() { bundleValidator.validate(appBundle); ImmutableMap sdkBundleModules = getValidatedSdkModules(closer, tempDir, appBundle); + bundleValidator.validateBundleWithSdkModules(appBundle, sdkBundleModules); AppBundlePreprocessorManager appBundlePreprocessorManager = DaggerAppBundlePreprocessorComponent.builder() 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 6fe365b6..873794c7 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 @@ -364,7 +364,6 @@ private ApkGenerationConfiguration.Builder getCommonSplitApkGenerationConfigurat .getMinSdkForAdditionalVariantWithV3Rotation() .ifPresent(apkGenerationConfiguration::setMinSdkForAdditionalVariantWithV3Rotation); - return apkGenerationConfiguration; } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java index 2c6a9a9e..5d2a5d2e 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommand.java @@ -98,7 +98,7 @@ public enum OutputFormat { abstract Optional getSdkArchivePath(); - abstract Integer getVersionCode(); + abstract int getVersionCode(); abstract Path getOutputFile(); @@ -126,7 +126,6 @@ ListeningExecutorService getExecutorService() { public abstract Optional getFirstVariantNumber(); - public abstract Optional getMinSdkVersion(); /** Creates a builder for the {@link BuildSdkApksCommand} with some default settings. */ @@ -150,7 +149,7 @@ public abstract static class Builder { public abstract Builder setSdkArchivePath(Path sdkArchivePath); /** Sets the SDK version code */ - public abstract Builder setVersionCode(Integer versionCode); + public abstract Builder setVersionCode(int versionCode); /** * Sets path to the output produced by the command. Depends on the output format: @@ -234,7 +233,6 @@ public Builder setExecutorService(ListeningExecutorService executorService) { */ public abstract Builder setFirstVariantNumber(int firstVariantNumber); - /** Overrides value of android:minSdkVersion attribute in the generated APKs. */ public abstract Builder setMinSdkVersion(int minSdkVersion); diff --git a/src/main/java/com/android/tools/build/bundletool/commands/DumpManager.java b/src/main/java/com/android/tools/build/bundletool/commands/DumpManager.java index 250d0073..4c96e560 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/DumpManager.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/DumpManager.java @@ -15,11 +15,8 @@ */ package com.android.tools.build.bundletool.commands; -import static com.android.tools.build.bundletool.model.utils.CollectorUtils.groupingBySortedKeys; import static com.google.common.collect.ImmutableList.toImmutableList; -import com.android.aapt.ConfigurationOuterClass.Configuration; -import com.android.aapt.Resources.ConfigValue; import com.android.aapt.Resources.ResourceTable; import com.android.aapt.Resources.XmlNode; import com.android.bundle.Config.BundleConfig; @@ -29,36 +26,18 @@ import com.android.tools.build.bundletool.model.BundleModuleName; import com.android.tools.build.bundletool.model.ResourceTableEntry; 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.ResourcesUtils; import com.android.tools.build.bundletool.model.utils.ZipUtils; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; -import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoPrintUtils; -import com.android.tools.build.bundletool.xml.XPathResolver; -import com.android.tools.build.bundletool.xml.XPathResolver.XPathResult; -import com.android.tools.build.bundletool.xml.XmlNamespaceContext; -import com.android.tools.build.bundletool.xml.XmlProtoToXmlConverter; -import com.android.tools.build.bundletool.xml.XmlUtils; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; import com.google.protobuf.util.JsonFormat; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.Optional; import java.util.function.Predicate; -import java.util.zip.ZipEntry; -import java.util.zip.ZipException; import java.util.zip.ZipFile; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import org.w3c.dom.Document; final class DumpManager { @@ -75,32 +54,11 @@ void printManifest(BundleModuleName moduleName, Optional xPathExpression ZipPath manifestPath = ZipPath.create(moduleName.getName()).resolve(SpecialModuleEntry.ANDROID_MANIFEST.getPath()); XmlProtoNode manifestProto = - new XmlProtoNode(extractAndParse(bundlePath, manifestPath, XmlNode::parseFrom)); + new XmlProtoNode( + DumpManagerUtils.extractAndParseFromAppBundle( + bundlePath, manifestPath, XmlNode::parseFrom)); - // Convert the proto to real XML. - Document document = XmlProtoToXmlConverter.convert(manifestProto); - - // Select the output. - String output; - if (xPathExpression.isPresent()) { - try { - XPath xPath = XPathFactory.newInstance().newXPath(); - xPath.setNamespaceContext(new XmlNamespaceContext(manifestProto)); - XPathExpression compiledXPathExpression = xPath.compile(xPathExpression.get()); - XPathResult xPathResult = XPathResolver.resolve(document, compiledXPathExpression); - output = xPathResult.toString(); - } catch (XPathExpressionException e) { - throw InvalidCommandException.builder() - .withInternalMessage("Error in the XPath expression: " + xPathExpression) - .withCause(e) - .build(); - } - } else { - output = XmlUtils.documentToString(document); - } - - // Print the output. - printStream.println(output.trim()); + DumpManagerUtils.printManifest(manifestProto, xPathExpression, printStream); } void printResources(Predicate resourcePredicate, boolean printValues) { @@ -109,30 +67,22 @@ void printResources(Predicate resourcePredicate, boolean pri resourceTables = ZipUtils.allFileEntriesPaths(zipFile) .filter(path -> path.endsWith(SpecialModuleEntry.RESOURCE_TABLE.getPath())) - .map(path -> extractAndParse(zipFile, path, ResourceTable::parseFrom)) + .map( + path -> DumpManagerUtils.extractAndParse(zipFile, path, ResourceTable::parseFrom)) .collect(toImmutableList()); } catch (IOException e) { throw new UncheckedIOException("Error occurred when reading the bundle.", e); } - ImmutableListMultimap entriesByPackageName = - resourceTables.stream() - .flatMap(ResourcesUtils::entries) - .filter(resourcePredicate) - .collect(groupingBySortedKeys(entry -> entry.getPackage().getPackageName())); - - for (String packageName : entriesByPackageName.keySet()) { - printStream.printf("Package '%s':%n", packageName); - entriesByPackageName.get(packageName).forEach(entry -> printEntry(entry, printValues)); - printStream.println(); - } + DumpManagerUtils.printResources(resourcePredicate, printValues, resourceTables, printStream); } void printBundleConfig() { try (ZipFile zipFile = new ZipFile(bundlePath.toFile())) { BundleConfig bundleConfig = - extractAndParse(zipFile, ZipPath.create("BundleConfig.pb"), BundleConfig::parseFrom); - printStream.println(JsonFormat.printer().print(bundleConfig)); + DumpManagerUtils.extractAndParse( + zipFile, ZipPath.create("BundleConfig.pb"), BundleConfig::parseFrom); + DumpManagerUtils.printBundleConfig(bundleConfig, printStream); } catch (IOException e) { throw new UncheckedIOException("Error occurred when reading the bundle.", e); } @@ -150,64 +100,4 @@ void printRuntimeEnabledSdkConfig() { throw new UncheckedIOException("Error occurred when reading the bundle.", e); } } - - private void printEntry(ResourceTableEntry entry, boolean printValues) { - printStream.printf( - "0x%08x - %s/%s%n", - entry.getResourceId().getFullResourceId(), - entry.getType().getName(), - entry.getEntry().getName()); - - for (ConfigValue configValue : entry.getEntry().getConfigValueList()) { - printStream.print('\t'); - if (configValue.getConfig().equals(Configuration.getDefaultInstance())) { - printStream.print("(default)"); - } else { - printStream.print(configValue.getConfig().toString().trim()); - } - if (printValues) { - printStream.printf( - " - [%s] %s", - XmlProtoPrintUtils.getValueTypeAsString(configValue.getValue()), - XmlProtoPrintUtils.getValueAsString(configValue.getValue())); - } - printStream.println(); - } - } - - private static T extractAndParse( - Path bundlePath, ZipPath filePath, ProtoParser protoParser) { - try (ZipFile zipFile = new ZipFile(bundlePath.toFile())) { - return extractAndParse(zipFile, filePath, protoParser); - } catch (ZipException e) { - throw InvalidBundleException.builder() - .withUserMessage("Bundle is not a valid zip file.") - .withCause(e) - .build(); - } catch (IOException e) { - throw new UncheckedIOException("Error occurred when trying to open the bundle.", e); - } - } - - private static T extractAndParse( - ZipFile zipFile, ZipPath filePath, ProtoParser protoParser) { - ZipEntry fileEntry = zipFile.getEntry(filePath.toString()); - if (fileEntry == null) { - throw InvalidBundleException.builder() - .withUserMessage("File '%s' not found.", filePath) - .build(); - } - - try (InputStream inputStream = zipFile.getInputStream(fileEntry)) { - return protoParser.parse(inputStream); - } catch (IOException e) { - throw new UncheckedIOException( - "Error occurred when trying to read file '" + filePath + "' from bundle.", e); - } - } - - /** Parser of a compiled proto from an {@link InputStream}. */ - private interface ProtoParser { - T parse(InputStream is) throws IOException; - } } diff --git a/src/main/java/com/android/tools/build/bundletool/commands/DumpManagerUtils.java b/src/main/java/com/android/tools/build/bundletool/commands/DumpManagerUtils.java new file mode 100644 index 00000000..8e973e53 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/DumpManagerUtils.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.utils.CollectorUtils.groupingBySortedKeys; + +import com.android.aapt.ConfigurationOuterClass.Configuration; +import com.android.aapt.Resources.ConfigValue; +import com.android.aapt.Resources.ResourceTable; +import com.android.tools.build.bundletool.model.ResourceTableEntry; +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.ResourcesUtils; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoPrintUtils; +import com.android.tools.build.bundletool.xml.XPathResolver; +import com.android.tools.build.bundletool.xml.XPathResolver.XPathResult; +import com.android.tools.build.bundletool.xml.XmlNamespaceContext; +import com.android.tools.build.bundletool.xml.XmlProtoToXmlConverter; +import com.android.tools.build.bundletool.xml.XmlUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.util.JsonFormat; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Document; + +/** Utility class for the dump commands {@link DumpCommand} and {@link DumpSdkBundleCommand}. */ +public final class DumpManagerUtils { + + public static void printManifest( + XmlProtoNode manifestProto, Optional xPathExpression, PrintStream printStream) { + + // Convert the proto to real XML. + Document document = XmlProtoToXmlConverter.convert(manifestProto); + + // Select the output. + String output; + if (xPathExpression.isPresent()) { + try { + XPath xPath = XPathFactory.newInstance().newXPath(); + xPath.setNamespaceContext(new XmlNamespaceContext(manifestProto)); + XPathExpression compiledXPathExpression = xPath.compile(xPathExpression.get()); + XPathResult xPathResult = XPathResolver.resolve(document, compiledXPathExpression); + output = xPathResult.toString(); + } catch (XPathExpressionException e) { + throw InvalidCommandException.builder() + .withInternalMessage("Error in the XPath expression: " + xPathExpression) + .withCause(e) + .build(); + } + } else { + output = XmlUtils.documentToString(document); + } + + // Print the output. + printStream.println(output.trim()); + } + + public static void printBundleConfig(MessageOrBuilder bundleConfig, PrintStream printStream) { + try { + printStream.println(JsonFormat.printer().print(bundleConfig)); + } catch (IOException e) { + throw new UncheckedIOException("Error occurred when reading the bundle.", e); + } + } + + public static void printResources( + Predicate resourcePredicate, + boolean printValues, + ImmutableList resourceTables, + PrintStream printStream) { + ImmutableListMultimap entriesByPackageName = + resourceTables.stream() + .flatMap(ResourcesUtils::entries) + .filter(resourcePredicate) + .collect(groupingBySortedKeys(entry -> entry.getPackage().getPackageName())); + + for (String packageName : entriesByPackageName.keySet()) { + printStream.printf("Package '%s':%n", packageName); + entriesByPackageName + .get(packageName) + .forEach(entry -> printEntry(entry, printValues, printStream)); + printStream.println(); + } + } + + private static void printEntry( + ResourceTableEntry entry, boolean printValues, PrintStream printStream) { + printStream.printf( + "0x%08x - %s/%s%n", + entry.getResourceId().getFullResourceId(), + entry.getType().getName(), + entry.getEntry().getName()); + + for (ConfigValue configValue : entry.getEntry().getConfigValueList()) { + printStream.print('\t'); + if (configValue.getConfig().equals(Configuration.getDefaultInstance())) { + printStream.print("(default)"); + } else { + printStream.print(configValue.getConfig().toString().trim()); + } + if (printValues) { + printStream.printf( + " - [%s] %s", + XmlProtoPrintUtils.getValueTypeAsString(configValue.getValue()), + XmlProtoPrintUtils.getValueAsString(configValue.getValue())); + } + printStream.println(); + } + } + + public static T extractAndParseFromSdkBundle( + Path bundlePath, ZipPath filePath, ProtoParser protoParser) { + try (ZipFile zipFile = new ZipFile(bundlePath.toFile())) { + ZipEntry modulesFile = zipFile.getEntry("modules.resm"); + ZipInputStream zipInputStream = new ZipInputStream(zipFile.getInputStream(modulesFile)); + return extractAndParse(zipInputStream, filePath, protoParser); + } catch (ZipException e) { + throw InvalidBundleException.builder() + .withUserMessage("Bundle is not a valid zip file.") + .withCause(e) + .build(); + } catch (IOException e) { + throw new UncheckedIOException("Error occurred when trying to open the bundle.", e); + } + } + + public static T extractAndParseFromAppBundle( + Path bundlePath, ZipPath filePath, ProtoParser protoParser) { + try (ZipFile zipFile = new ZipFile(bundlePath.toFile())) { + return extractAndParse(zipFile, filePath, protoParser); + } catch (ZipException e) { + throw InvalidBundleException.builder() + .withUserMessage("Bundle is not a valid zip file.") + .withCause(e) + .build(); + } catch (IOException e) { + throw new UncheckedIOException("Error occurred when trying to open the bundle.", e); + } + } + + public static T extractAndParse( + ZipInputStream zipInputStream, ZipPath filePath, ProtoParser protoParser) { + try { + ZipEntry zipEntry; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (zipEntry.getName().equals(filePath.toString())) { + return protoParser.parse(zipInputStream); + } + } + } catch (IOException e) { + throw new UncheckedIOException( + "Error occurred when trying to read file '" + filePath + "' from bundle.", e); + } + throw InvalidBundleException.builder() + .withUserMessage("File '%s' not found.", filePath) + .build(); + } + + public static T extractAndParse( + ZipFile zipFile, ZipPath filePath, ProtoParser protoParser) { + ZipEntry fileEntry = zipFile.getEntry(filePath.toString()); + if (fileEntry == null) { + throw InvalidBundleException.builder() + .withUserMessage("File '%s' not found.", filePath) + .build(); + } + + try (InputStream inputStream = zipFile.getInputStream(fileEntry)) { + return protoParser.parse(inputStream); + } catch (IOException e) { + throw new UncheckedIOException( + "Error occurred when trying to read file '" + filePath + "' from bundle.", e); + } + } + + private DumpManagerUtils() {} + + /** Parser of a compiled proto from an {@link InputStream}. */ + public interface ProtoParser { + T parse(InputStream is) throws IOException; + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/DumpSdkBundleCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/DumpSdkBundleCommand.java new file mode 100644 index 00000000..d71750f7 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/DumpSdkBundleCommand.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.util.function.Function.identity; + +import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription; +import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription; +import com.android.tools.build.bundletool.flags.Flag; +import com.android.tools.build.bundletool.flags.ParsedFlags; +import com.android.tools.build.bundletool.model.ResourceTableEntry; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Command that prints information about a given Android SDK Bundle. */ +@AutoValue +public abstract class DumpSdkBundleCommand { + + public static final String COMMAND_NAME = "dump-sdk-bundle"; + + private static final Flag BUNDLE_LOCATION_FLAG = Flag.path("bundle"); + private static final Flag XPATH_FLAG = Flag.string("xpath"); + private static final Flag RESOURCE_FLAG = Flag.string("resource"); + private static final Flag VALUES_FLAG = Flag.booleanFlag("values"); + + private static final Pattern RESOURCE_NAME_PATTERN = + Pattern.compile("(?[^/]+)/(?[^/]+)"); + + public abstract Path getBundlePath(); + + public abstract PrintStream getOutputStream(); + + public abstract DumpTarget getDumpTarget(); + + public abstract Optional getXPathExpression(); + + public abstract Optional getResourceId(); + + public abstract Optional getResourceName(); + + public abstract Optional getPrintValues(); + + public static Builder builder() { + return new AutoValue_DumpSdkBundleCommand.Builder().setOutputStream(System.out); + } + + /** Builder for the {@link DumpSdkBundleCommand}. */ + @AutoValue.Builder + public abstract static class Builder { + /** Sets the path to the bundle. */ + public abstract Builder setBundlePath(Path bundlePath); + + /** Sets the output stream where the dump should be printed. */ + public abstract Builder setOutputStream(PrintStream outputStream); + + /** Sets the target of the dump, e.g. the manifest. */ + public abstract Builder setDumpTarget(DumpTarget dumpTarget); + + /** Sets the XPath expression used to extract only part of the XML file being printed. */ + public abstract Builder setXPathExpression(String xPathExpression); + + /** + * Sets the ID of the resource to print. + * + *

Mutually exclusive with {@link #setResourceName}. + */ + public abstract Builder setResourceId(int resourceId); + + /** + * Sets the name of the resource to print. Must have the format "/", e.g. + * "drawable/icon". + * + *

Mutually exclusive with {@link #setResourceId}. + */ + public abstract Builder setResourceName(String resourceName); + + /** Sets whether the values should also be printed when printing the resources. */ + public abstract Builder setPrintValues(boolean printValues); + + public abstract DumpSdkBundleCommand build(); + } + + public static DumpSdkBundleCommand fromFlags(ParsedFlags flags) { + DumpTarget dumpTarget = parseDumpTarget(flags); + + Path bundlePath = BUNDLE_LOCATION_FLAG.getRequiredValue(flags); + Optional xPath = XPATH_FLAG.getValue(flags); + Optional resource = RESOURCE_FLAG.getValue(flags); + Optional printValues = VALUES_FLAG.getValue(flags); + + DumpSdkBundleCommand.Builder dumpCommand = + DumpSdkBundleCommand.builder().setBundlePath(bundlePath).setDumpTarget(dumpTarget); + + xPath.ifPresent(dumpCommand::setXPathExpression); + printValues.ifPresent(dumpCommand::setPrintValues); + resource.ifPresent( + r -> { + try { + // Using Long.decode to support negative resource IDs specified in hexadecimal. + dumpCommand.setResourceId(Long.decode(r).intValue()); + } catch (NumberFormatException e) { + dumpCommand.setResourceName(r); + } + }); + + return dumpCommand.build(); + } + + public void execute() { + validateInput(); + + switch (getDumpTarget()) { + case CONFIG: + new DumpSdkBundleManager(getOutputStream(), getBundlePath()).printBundleConfig(); + break; + + case MANIFEST: + new DumpSdkBundleManager(getOutputStream(), getBundlePath()) + .printManifest(getXPathExpression()); + break; + + case RESOURCES: + new DumpSdkBundleManager(getOutputStream(), getBundlePath()) + .printResources(parseResourcePredicate(), getPrintValues().orElse(false)); + break; + } + } + + private void validateInput() { + checkFileExistsAndReadable(getBundlePath()); + + if (getResourceId().isPresent() && getResourceName().isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage("Cannot pass both resource ID and resource name. Pick one!") + .build(); + } + if (getDumpTarget().equals(DumpTarget.RESOURCES) && getXPathExpression().isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage("Cannot pass an XPath expression when dumping resources.") + .build(); + } + if (!getDumpTarget().equals(DumpTarget.RESOURCES) + && (getResourceId().isPresent() || getResourceName().isPresent())) { + throw InvalidCommandException.builder() + .withInternalMessage("The resource name/id can only be passed when dumping resources.") + .build(); + } + if (!getDumpTarget().equals(DumpTarget.RESOURCES) && getPrintValues().isPresent()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Printing resource values can only be requested when dumping resources.") + .build(); + } + } + + private static DumpTarget parseDumpTarget(ParsedFlags flags) { + String subCommand = + flags + .getSubCommand() + .orElseThrow( + () -> + InvalidCommandException.builder() + .withInternalMessage("Target of the dump not found.") + .build()); + + return DumpTarget.fromString(subCommand); + } + + private Predicate parseResourcePredicate() { + if (getResourceId().isPresent()) { + return entry -> entry.getResourceId().getFullResourceId() == getResourceId().get().intValue(); + } + + if (getResourceName().isPresent()) { + String resourceName = getResourceName().get(); + Matcher matcher = RESOURCE_NAME_PATTERN.matcher(resourceName); + if (!matcher.matches()) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Resource name must match the format '/', e.g. 'drawable/icon'.") + .build(); + } + return entry -> + entry.getType().getName().equals(matcher.group("type")) + && entry.getEntry().getName().equals(matcher.group("name")); + } + + return entry -> true; + } + + /** Target of the dump. */ + public enum DumpTarget { + MANIFEST("manifest"), + RESOURCES("resources"), + CONFIG("config"); + + static final ImmutableMap SUBCOMMAND_TO_TARGET = + Arrays.stream(DumpTarget.values()) + .collect(toImmutableMap(DumpTarget::toString, identity())); + + private final String subCommand; + + DumpTarget(String subCommand) { + this.subCommand = subCommand; + } + + @Override + public String toString() { + return subCommand; + } + + public static DumpTarget fromString(String subCommand) { + DumpTarget dumpTarget = SUBCOMMAND_TO_TARGET.get(subCommand); + if (dumpTarget == null) { + throw InvalidCommandException.builder() + .withInternalMessage( + "Unrecognized dump target: '%s'. Accepted values are: %s", + subCommand, SUBCOMMAND_TO_TARGET.keySet()) + .build(); + } + return dumpTarget; + } + } + + public static CommandHelp help() { + return CommandHelp.builder() + .setCommandName(COMMAND_NAME) + .setSubCommandNames(DumpTarget.SUBCOMMAND_TO_TARGET.keySet().asList()) + .setCommandDescription( + CommandDescription.builder() + .setShortDescription( + "Prints files or extract values from the SDK bundle in a human-readable form.") + .addAdditionalParagraph("Examples:") + .addAdditionalParagraph( + String.format( + "1. Prints the AndroidManifest.xml of the SDK bundle:%n" + + "$ bundletool dump-sdk-bundle manifest --bundle=/tmp/sdk.asb")) + .addAdditionalParagraph( + String.format( + "2. Prints the package of the SDK bundle:%n" + + "$ bundletool dump-sdk-bundle manifest --bundle=/tmp/sdk.asb " + + "--xpath=/manifest/@package")) + .addAdditionalParagraph( + String.format( + "3. Prints all the resources present in the SDK bundle:%n" + + "$ bundletool dump-sdk-bundle resources --bundle=/tmp/sdk.asb")) + .addAdditionalParagraph( + String.format( + "4. Prints a resource's configs from its resource ID:%n" + + "$ bundletool dump-sdk-bundle resources --bundle=/tmp/sdk.asb " + + "--resource=0x7f0e013a")) + .addAdditionalParagraph( + String.format( + "5. Prints a resource's configs and values from its resource type & name:%n" + + "$ bundletool dump-sdk-bundle resources --bundle=/tmp/sdk.asb " + + "--resource=drawable/icon --values")) + .addAdditionalParagraph( + String.format( + "6. Prints the content of the SDK bundle configuration file:%n" + + "$ bundletool dump-sdk-bundle config --bundle=/tmp/sdk.asb")) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName("bundle") + .setDescription("Path to the SDK Bundle.") + .setExampleValue("sdk.asb") + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName("xpath") + .setDescription( + "XPath expression to extract the value of attributes from the XML file being " + + "dumped. Only applies when dumping the manifest.") + .setExampleValue("/manifest/@package") + .setOptional(true) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName("resource") + .setDescription( + "Name or ID of the resource to lookup. Only applies when dumping resources. If " + + "a resource ID is provided, it can be specified either as a decimal or " + + "hexadecimal integer. If a resource name is provided, it must follow the " + + "format '/', e.g. 'drawable/icon'") + .setExampleValue("0x7f030001") + .setOptional(true) + .build()) + .addFlag( + FlagDescription.builder() + .setFlagName("values") + .setDescription( + "When set, also prints the values of the resources. Defaults to false. " + + "Only applies when dumping the resources.") + .setOptional(true) + .build()) + .build(); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/DumpSdkBundleManager.java b/src/main/java/com/android/tools/build/bundletool/commands/DumpSdkBundleManager.java new file mode 100644 index 00000000..c0f55342 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/commands/DumpSdkBundleManager.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.commands; + +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.XmlNode; +import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; +import com.android.tools.build.bundletool.model.BundleModule.SpecialModuleEntry; +import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.ResourceTableEntry; +import com.android.tools.build.bundletool.model.ZipPath; +import com.android.tools.build.bundletool.model.utils.ZipUtils; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoNode; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +final class DumpSdkBundleManager { + + private final PrintStream printStream; + private final Path bundlePath; + + DumpSdkBundleManager(OutputStream outputStream, Path bundlePath) { + this.printStream = new PrintStream(outputStream); + this.bundlePath = bundlePath; + } + + void printManifest(Optional xPathExpression) { + // Extract the manifest from the bundle. + ZipPath manifestPath = + ZipPath.create(BundleModuleName.BASE_MODULE_NAME.getName()) + .resolve(SpecialModuleEntry.ANDROID_MANIFEST.getPath()); + XmlProtoNode manifestProto = + new XmlProtoNode( + DumpManagerUtils.extractAndParseFromSdkBundle( + bundlePath, manifestPath, XmlNode::parseFrom)); + + DumpManagerUtils.printManifest(manifestProto, xPathExpression, printStream); + } + + void printResources(Predicate resourcePredicate, boolean printValues) { + ImmutableList resourceTables; + try (ZipFile zipFile = new ZipFile(bundlePath.toFile())) { + ZipEntry zipEntry = zipFile.getEntry("modules.resm"); + resourceTables = + ZipUtils.allFileEntriesPaths(new ZipInputStream(zipFile.getInputStream(zipEntry))) + .stream() + .filter(path -> path.endsWith(SpecialModuleEntry.RESOURCE_TABLE.getPath())) + .map( + path -> + DumpManagerUtils.extractAndParseFromSdkBundle( + bundlePath, path, ResourceTable::parseFrom)) + .collect(toImmutableList()); + } catch (IOException e) { + throw new UncheckedIOException("Error occurred when reading the bundle.", e); + } + DumpManagerUtils.printResources(resourcePredicate, printValues, resourceTables, printStream); + } + + void printBundleConfig() { + SdkModulesConfig bundleConfig = + DumpManagerUtils.extractAndParseFromSdkBundle( + bundlePath, ZipPath.create("SdkModulesConfig.pb"), SdkModulesConfig::parseFrom); + DumpManagerUtils.printBundleConfig(bundleConfig, printStream); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java b/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java index 40e67398..c75b7cd1 100644 --- a/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java +++ b/src/main/java/com/android/tools/build/bundletool/commands/ValidateBundleCommand.java @@ -44,7 +44,7 @@ public abstract class ValidateBundleCommand { public abstract Path getBundlePath(); - public abstract Boolean getPrintOutput(); + public abstract boolean getPrintOutput(); public static Builder builder() { return new AutoValue_ValidateBundleCommand.Builder().setPrintOutput(false); @@ -55,7 +55,7 @@ public static Builder builder() { public abstract static class Builder { public abstract Builder setBundlePath(Path bundlePath); - public abstract Builder setPrintOutput(Boolean printOutput); + public abstract Builder setPrintOutput(boolean printOutput); public abstract ValidateBundleCommand build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java index e43efe31..a3d640de 100644 --- a/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java +++ b/src/main/java/com/android/tools/build/bundletool/io/ApkSerializerManager.java @@ -406,6 +406,10 @@ private AssetModuleMetadata getAssetModuleMetadata(BundleModule module) { persistentDelivery .map(delivery -> getDeliveryType(delivery)) .orElse(DeliveryType.INSTALL_TIME)); + persistentDelivery + .map(ManifestDeliveryElement::getAssetModuleConditions) + .ifPresent(metadataBuilder::setTargeting); + // The module is instant if either the dist:instant attribute is true or the // dist:instant-delivery element is present. boolean isInstantModule = module.isInstantModule(); 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 4ca41388..e01393f5 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 @@ -91,7 +91,10 @@ public abstract class AndroidManifest { public static final String USES_FEATURE_ELEMENT_NAME = "uses-feature"; public static final String MODULE_ELEMENT_NAME = "module"; public static final String DELIVERY_ELEMENT_NAME = "delivery"; + public static final String CONDITIONS_ELEMENT_NAME = "conditions"; public static final String INSTALL_TIME_ELEMENT_NAME = "install-time"; + public static final String FAST_FOLLOW_ELEMENT_NAME = "fast-follow"; + public static final String ON_DEMAND_ELEMENT_NAME = "on-demand"; public static final String REMOVABLE_ELEMENT_NAME = "removable"; public static final String FUSING_ELEMENT_NAME = "fusing"; public static final String STYLE_ELEMENT_NAME = "style"; @@ -122,7 +125,6 @@ public abstract class AndroidManifest { public static final String SPLIT_NAME_ATTRIBUTE_NAME = "splitName"; public static final String VERSION_NAME_ATTRIBUTE_NAME = "versionName"; public static final String INSTALL_LOCATION_ATTRIBUTE_NAME = "installLocation"; - public static final String IS_SPLIT_REQUIRED_ATTRIBUTE_NAME = "isSplitRequired"; public static final String SHARED_USER_ID_ATTRIBUTE_NAME = "sharedUserId"; public static final String SHARED_USER_LABEL_ATTRIBUTE_NAME = "sharedUserLabel"; public static final String DESCRIPTION_ATTRIBUTE_NAME = "description"; @@ -178,6 +180,7 @@ public abstract class AndroidManifest { public static final String SRC_ATTRIBUTE_NAME = "src"; public static final String APP_COMPONENT_FACTORY_ATTRIBUTE_NAME = "appComponentFactory"; public static final String AUTHORITIES_ATTRIBUTE_NAME = "authorities"; + public static final String PROCESS_ATTRIBUTE_NAME = "process"; public static final String LEANBACK_FEATURE_NAME = "android.software.leanback"; public static final String TOUCHSCREEN_FEATURE_NAME = "android.hardware.touchscreen"; @@ -214,7 +217,6 @@ public abstract class AndroidManifest { public static final int TARGET_SANDBOX_VERSION_RESOURCE_ID = 0x0101054c; public static final int SPLIT_NAME_RESOURCE_ID = 0x01010549; public static final int INSTALL_LOCATION_RESOURCE_ID = 0x010102b7; - public static final int IS_SPLIT_REQUIRED_RESOURCE_ID = 0x01010591; public static final int THEME_RESOURCE_ID = 0x01010000; public static final int ISOLATED_SPLITS_ID = 0x0101054b; public static final int SHARED_USER_ID_RESOURCE_ID = 0x0101000b; @@ -254,6 +256,7 @@ public abstract class AndroidManifest { public static final int SPLIT_TYPES_RESOURCE_ID = 0x0101064f; public static final int REQUIRED_SPLIT_TYPES_RESOURCE_ID = 0x0101064e; public static final int AUTO_VERIFY_RESOURCE_ID = 0x010104ee; + public static final int PROCESS_RESOURCE_ID = 0x01010011; // 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. @@ -289,16 +292,13 @@ public XmlProtoElement getManifestElement() { @Memoized public Optional getManifestDeliveryElement() { - return ManifestDeliveryElement.fromManifestElement( - getManifestElement(), - /* isFastFollowAllowed= */ getModuleType().equals(ModuleType.ASSET_MODULE)); + return ManifestDeliveryElement.fromManifestElement(getManifestElement(), getModuleType()); } @Memoized public Optional getInstantManifestDeliveryElement() { return ManifestDeliveryElement.instantFromManifestElement( - getManifestElement(), - /* isFastFollowAllowed= */ getModuleType().equals(ModuleType.ASSET_MODULE)); + getManifestElement(), getModuleType()); } /** @@ -381,9 +381,9 @@ public AndroidManifest applyMutators(ImmutableList manifestMuta * @return An optional containing the value of the {@code appCategory} attribute if set, or an * empty optional if not set. */ - public Optional getApplicationAppCategory() { + public Optional getApplicationAppCategory() { return getApplicationAttribute(APP_CATEGORY_RESOURCE_ID) - .map(XmlProtoAttribute::getValueAsString); + .map(XmlProtoAttribute::getValueAsDecimalInteger); } /** @@ -427,12 +427,12 @@ public Optional getMinSdkVersion() { return getUsesSdkAttribute(MIN_SDK_VERSION_RESOURCE_ID); } - public Optional getTargetingSdkVersion() { + public Optional getTargetSdkVersion() { return getUsesSdkAttribute(TARGET_SDK_VERSION_RESOURCE_ID); } - public int getEffectiveTargetingSdkVersion() { - return getTargetingSdkVersion().orElse(getEffectiveMinSdkVersion()); + public int getEffectiveTargetSdkVersion() { + return getTargetSdkVersion().orElse(getEffectiveMinSdkVersion()); } public int getEffectiveMinSdkVersion() { @@ -728,19 +728,6 @@ public Optional getExtractNativeLibsValue() { return getApplicationAttributeAsBoolean(EXTRACT_NATIVE_LIBS_RESOURCE_ID); } - /** - * Extracts the 'android:isSplitRequired' value from the {@code } tag. - * - *

Warning: this value is not read by the system and is provided for legacy install verifiers - * only. - * - * @return An optional containing the value of the 'isSplitRequired' attribute if set, or an empty - * optional if not set. - */ - public Optional getSplitsRequiredValue() { - return getApplicationAttributeAsBoolean(IS_SPLIT_REQUIRED_RESOURCE_ID); - } - /** Extracts the 'android:splitTypes' value from the {@code } tag. */ public Optional> getProvidedSplitTypesValue() { return getManifestElement() diff --git a/src/main/java/com/android/tools/build/bundletool/model/DeviceGroupsCondition.java b/src/main/java/com/android/tools/build/bundletool/model/DeviceGroupsCondition.java index e9922be7..f8a82a75 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/DeviceGroupsCondition.java +++ b/src/main/java/com/android/tools/build/bundletool/model/DeviceGroupsCondition.java @@ -15,6 +15,7 @@ */ package com.android.tools.build.bundletool.model; +import com.android.bundle.Targeting.DeviceGroupModuleTargeting; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; @@ -28,4 +29,8 @@ public abstract class DeviceGroupsCondition { public static DeviceGroupsCondition create(ImmutableSet deviceGroups) { return new AutoValue_DeviceGroupsCondition(deviceGroups); } + + public DeviceGroupModuleTargeting toTargeting() { + return DeviceGroupModuleTargeting.newBuilder().addAllValue(getDeviceGroups()).build(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java index 4619f574..d939c4ca 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestDeliveryElement.java @@ -17,6 +17,7 @@ package com.android.tools.build.bundletool.model; import static com.android.tools.build.bundletool.model.AndroidManifest.CODE_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.CONDITIONS_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.CONDITION_DEVICE_FEATURE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.CONDITION_DEVICE_GROUPS_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.CONDITION_MAX_SDK_VERSION_NAME; @@ -26,13 +27,18 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.DEVICE_GROUP_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.DISTRIBUTION_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.EXCLUDE_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.FAST_FOLLOW_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_TIME_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.ON_DEMAND_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.VALUE_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.utils.CollectorUtils.groupingByDeterministic; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.stream.Collectors.counting; import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Targeting.AssetModuleTargeting; +import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.utils.DeviceTargetingUtils; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; @@ -46,9 +52,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.Immutable; -import java.util.LinkedHashSet; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; /** Parses and provides business logic utilities for element. */ @@ -58,10 +62,16 @@ public abstract class ManifestDeliveryElement { private static final String VERSION_ATTRIBUTE_NAME = "version"; - private static final ImmutableList KNOWN_DELIVERY_MODES = - ImmutableList.of("install-time", "on-demand", "fast-follow"); + private static final ImmutableList ASSET_MODULE_DELIVERY_ELEMENTS = + ImmutableList.of( + INSTALL_TIME_ELEMENT_NAME, + ON_DEMAND_ELEMENT_NAME, + FAST_FOLLOW_ELEMENT_NAME, + CONDITIONS_ELEMENT_NAME); + private static final ImmutableList FEATURE_MODULE_DELIVERY_ELEMENTS = + ImmutableList.of(INSTALL_TIME_ELEMENT_NAME, ON_DEMAND_ELEMENT_NAME); private static final ImmutableList KNOWN_INSTALL_TIME_ATTRIBUTES = - ImmutableList.of("conditions", "removable"); + ImmutableList.of(CONDITIONS_ELEMENT_NAME, "removable"); private static final ImmutableList CONDITIONS_ALLOWED_ONLY_ONCE = ImmutableList.of( CONDITION_MIN_SDK_VERSION_NAME, @@ -71,7 +81,7 @@ public abstract class ManifestDeliveryElement { abstract XmlProtoElement getDeliveryElement(); - abstract boolean isFastFollowAllowed(); + abstract ModuleType getModuleType(); /** * Returns if this element is well-formed. @@ -82,7 +92,7 @@ public abstract class ManifestDeliveryElement { public boolean isWellFormed() { return hasOnDemandElement() || hasInstallTimeElement() - || (isFastFollowAllowed() && hasFastFollowElement()); + || (getModuleType() == ModuleType.ASSET_MODULE && hasFastFollowElement()); } public boolean hasModuleConditions() { @@ -92,21 +102,24 @@ public boolean hasModuleConditions() { @Memoized public boolean hasOnDemandElement() { return getDeliveryElement() - .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "on-demand") + .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, ON_DEMAND_ELEMENT_NAME) .isPresent(); } public boolean hasFastFollowElement() { return getDeliveryElement() - .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "fast-follow") + .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, FAST_FOLLOW_ELEMENT_NAME) .isPresent(); } @Memoized public boolean hasInstallTimeElement() { + return getInstallTimeElement().isPresent(); + } + + private Optional getInstallTimeElement() { return getDeliveryElement() - .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time") - .isPresent(); + .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, INSTALL_TIME_ELEMENT_NAME); } /** @@ -117,8 +130,7 @@ public boolean hasInstallTimeElement() { * removable. */ public Optional getInstallTimeRemovableValue() { - return getDeliveryElement() - .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time") + return getInstallTimeElement() .flatMap( installTime -> installTime @@ -136,38 +148,16 @@ public Optional getInstallTimeRemovableValue() { + " namespace is also set.")))); } - /** - * Returns all module conditions. - * - *

We support , and - * conditions today. Any other conditions types are not supported and will result in {@link - * InvalidBundleException}. - */ + /** Returns all module conditions for install-time feature modules. */ @Memoized public ModuleConditions getModuleConditions() { - ImmutableList conditionElements = getModuleConditionElements(); - - ImmutableMap conditionCounts = - conditionElements.stream() - .collect(groupingByDeterministic(XmlProtoElement::getName, counting())); - for (String conditionName : CONDITIONS_ALLOWED_ONLY_ONCE) { - if (conditionCounts.getOrDefault(conditionName, 0L) > 1) { - throw InvalidBundleException.builder() - .withUserMessage("Multiple '' conditions are not supported.", conditionName) - .build(); - } - } + ImmutableList conditionElements = + getModuleConditionElements(getInstallTimeElement()); + verifyUniqueConditions(conditionElements); ModuleConditions.Builder moduleConditions = ModuleConditions.builder(); for (XmlProtoElement conditionElement : conditionElements) { - if (!conditionElement.getNamespaceUri().equals(DISTRIBUTION_NAMESPACE_URI)) { - throw InvalidBundleException.builder() - .withUserMessage( - "Invalid namespace found in the module condition element. " - + "Expected '%s'; found '%s'.", - DISTRIBUTION_NAMESPACE_URI, conditionElement.getNamespaceUri()) - .build(); - } + verifyDistributionNamespace(conditionElement); switch (conditionElement.getName()) { case CONDITION_DEVICE_FEATURE_NAME: moduleConditions.addDeviceFeatureCondition(parseDeviceFeatureCondition(conditionElement)); @@ -210,6 +200,59 @@ public ModuleConditions getModuleConditions() { return processedModuleConditions; } + /** Returns all module conditions for asset modules. */ + public AssetModuleTargeting getAssetModuleConditions() { + ImmutableList conditionElements = + getModuleConditionElements(Optional.of(getDeliveryElement())); + verifyUniqueConditions(conditionElements); + + AssetModuleTargeting.Builder targetingBuilder = AssetModuleTargeting.newBuilder(); + for (XmlProtoElement conditionElement : conditionElements) { + verifyDistributionNamespace(conditionElement); + switch (conditionElement.getName()) { + case CONDITION_USER_COUNTRIES_NAME: + targetingBuilder.setUserCountriesTargeting( + parseUserCountriesCondition(conditionElement).toTargeting()); + break; + case CONDITION_DEVICE_GROUPS_NAME: + targetingBuilder.setDeviceGroupTargeting( + parseDeviceGroupsCondition(conditionElement).toTargeting()); + break; + default: + throw InvalidBundleException.builder() + .withUserMessage("Unrecognized module condition: '%s'", conditionElement.getName()) + .build(); + } + } + + return targetingBuilder.build(); + } + + private static void verifyDistributionNamespace(XmlProtoElement conditionElement) { + if (!conditionElement.getNamespaceUri().equals(DISTRIBUTION_NAMESPACE_URI)) { + throw InvalidBundleException.builder() + .withUserMessage( + "Invalid namespace found in the module condition element. " + + "Expected '%s'; found '%s'.", + DISTRIBUTION_NAMESPACE_URI, conditionElement.getNamespaceUri()) + .build(); + } + } + + /** Verifies that unique delivery conditions are only specified once. */ + private static void verifyUniqueConditions(ImmutableList conditionElements) { + ImmutableMap conditionCounts = + conditionElements.stream() + .collect(groupingByDeterministic(XmlProtoElement::getName, counting())); + for (String conditionName : CONDITIONS_ALLOWED_ONLY_ONCE) { + if (conditionCounts.getOrDefault(conditionName, 0L) > 1) { + throw InvalidBundleException.builder() + .withUserMessage("Multiple '' conditions are not supported.", conditionName) + .build(); + } + } + } + private UserCountriesCondition parseUserCountriesCondition(XmlProtoElement conditionElement) { ImmutableList.Builder countryCodes = ImmutableList.builder(); for (XmlProtoElement countryElement : @@ -284,37 +327,40 @@ private DeviceGroupsCondition parseDeviceGroupsCondition(XmlProtoElement conditi } private static void validateDeliveryElement( - XmlProtoElement deliveryElement, boolean isFastFollowAllowed) { - validateDeliveryElementChildren(deliveryElement, isFastFollowAllowed); + XmlProtoElement deliveryElement, ModuleType moduleType) { + validateDeliveryElementChildren(deliveryElement, moduleType); validateInstallTimeElement( - deliveryElement.getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time")); + deliveryElement.getOptionalChildElement( + DISTRIBUTION_NAMESPACE_URI, INSTALL_TIME_ELEMENT_NAME)); validateOnDemandElement( - deliveryElement.getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "on-demand")); - if (isFastFollowAllowed) { + deliveryElement.getOptionalChildElement( + DISTRIBUTION_NAMESPACE_URI, ON_DEMAND_ELEMENT_NAME)); + if (moduleType == ModuleType.ASSET_MODULE) { validateFastFollowElement( - deliveryElement.getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "fast-follow")); + deliveryElement.getOptionalChildElement( + DISTRIBUTION_NAMESPACE_URI, FAST_FOLLOW_ELEMENT_NAME)); } } private static void validateDeliveryElementChildren( - XmlProtoElement deliveryElement, boolean isFastFollowAllowed) { - Set allowedDeliveryModes = new LinkedHashSet<>(KNOWN_DELIVERY_MODES); - if (!isFastFollowAllowed) { - allowedDeliveryModes.remove("fast-follow"); - } + XmlProtoElement deliveryElement, ModuleType moduleType) { + ImmutableList allowedDeliveryElements = + moduleType == ModuleType.ASSET_MODULE + ? ASSET_MODULE_DELIVERY_ELEMENTS + : FEATURE_MODULE_DELIVERY_ELEMENTS; Optional offendingElement = deliveryElement .getChildrenElements( child -> !(child.getNamespaceUri().equals(DISTRIBUTION_NAMESPACE_URI) - && allowedDeliveryModes.contains(child.getName()))) + && allowedDeliveryElements.contains(child.getName()))) .findAny(); if (offendingElement.isPresent()) { throw InvalidBundleException.builder() .withUserMessage( "Expected element to contain only %s elements but found: %s", - allowedDeliveryModes.stream() + allowedDeliveryElements.stream() .map(name -> String.format("", name)) .collect(Collectors.joining(", ")), printElement(offendingElement.get())) @@ -367,13 +413,13 @@ private static void validateFastFollowElement(Optional fastFoll } } - private ImmutableList getModuleConditionElements() { - Optional installTimeElement = - getDeliveryElement().getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time"); - return installTimeElement + private ImmutableList getModuleConditionElements( + Optional parentElement) { + return parentElement .flatMap( installTime -> - installTime.getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "conditions")) + installTime.getOptionalChildElement( + DISTRIBUTION_NAMESPACE_URI, CONDITIONS_ELEMENT_NAME)) .map(conditions -> conditions.getChildrenElements().collect(toImmutableList())) .orElse(ImmutableList.of()); } @@ -426,19 +472,19 @@ private static String printElement(XmlProtoElement element) { * contains the element. */ public static Optional fromManifestElement( - XmlProtoElement manifestElement, boolean isFastFollowAllowed) { - return fromManifestElement(manifestElement, "delivery", isFastFollowAllowed); + XmlProtoElement manifestElement, ModuleType moduleType) { + return fromManifestElement(manifestElement, "delivery", moduleType); } private static Optional fromManifestElement( - XmlProtoElement manifestElement, String deliveryTag, boolean isFastFollowAllowed) { + XmlProtoElement manifestElement, String deliveryTag, ModuleType moduleType) { return manifestElement .getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, "module") .flatMap(elem -> elem.getOptionalChildElement(DISTRIBUTION_NAMESPACE_URI, deliveryTag)) .map( (XmlProtoElement elem) -> { - validateDeliveryElement(elem, isFastFollowAllowed); - return new AutoValue_ManifestDeliveryElement(elem, isFastFollowAllowed); + validateDeliveryElement(elem, moduleType); + return new AutoValue_ManifestDeliveryElement(elem, moduleType); }); } @@ -447,19 +493,19 @@ private static Optional fromManifestElement( * the element. */ public static Optional instantFromManifestElement( - XmlProtoElement manifestElement, boolean isFastFollowAllowed) { - return fromManifestElement(manifestElement, "instant-delivery", isFastFollowAllowed); + XmlProtoElement manifestElement, ModuleType moduleType) { + return fromManifestElement(manifestElement, "instant-delivery", moduleType); } @VisibleForTesting static Optional fromManifestRootNode( - XmlNode xmlNode, boolean isFastFollowAllowed) { - return fromManifestElement(new XmlProtoNode(xmlNode).getElement(), isFastFollowAllowed); + XmlNode xmlNode, ModuleType moduleType) { + return fromManifestElement(new XmlProtoNode(xmlNode).getElement(), moduleType); } @VisibleForTesting static Optional instantFromManifestRootNode( - XmlNode xmlNode, boolean isFastFollowAllowed) { - return instantFromManifestElement(new XmlProtoNode(xmlNode).getElement(), isFastFollowAllowed); + XmlNode xmlNode, ModuleType moduleType) { + return instantFromManifestElement(new XmlProtoNode(xmlNode).getElement(), moduleType); } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java index 2db7c243..a4b58775 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ManifestEditor.java @@ -37,8 +37,6 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_TIME_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.INTENT_FILTER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_FEATURE_SPLIT_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_ATTRIBUTE_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.METADATA_KEY_SDK_PATCH_VERSION_PREFIX; @@ -208,12 +206,12 @@ public ManifestEditor setLocaleConfig(int resourceId) { } @CanIgnoreReturnValue - public ManifestEditor setAppCategory(String appCategory) { + public ManifestEditor setAppCategory(int appCategory) { manifestElement .getOrCreateChildElement(APPLICATION_ELEMENT_NAME) .getOrCreateAndroidAttribute( AndroidManifest.APP_CATEGORY_ATTRIBUTE_NAME, AndroidManifest.APP_CATEGORY_RESOURCE_ID) - .setValueAsString(appCategory); + .setValueAsDecimalInteger(appCategory); return this; } @@ -314,17 +312,13 @@ public ManifestEditor setFusedModuleNames(ImmutableList moduleNames) { *

*/ @CanIgnoreReturnValue public ManifestEditor setSplitsRequired(boolean value) { - setMetadataValue( + return setMetadataValue( META_DATA_KEY_SPLITS_REQUIRED, createAndroidAttribute("value", VALUE_RESOURCE_ID).setValueAsBoolean(value)); - return setApplcationAttributeBoolean( - IS_SPLIT_REQUIRED_ATTRIBUTE_NAME, IS_SPLIT_REQUIRED_RESOURCE_ID, value); } /** @@ -334,16 +328,9 @@ public ManifestEditor setSplitsRequired(boolean value) { * requiredSplitTypes} to perform validation at install time. */ @CanIgnoreReturnValue - public ManifestEditor setSplitTypes( - ImmutableList splitTypes, boolean enableSystemAttribute) { - if (enableSystemAttribute) { - manifestElement - .getOrCreateAndroidAttribute(SPLIT_TYPES_ATTRIBUTE_NAME, SPLIT_TYPES_RESOURCE_ID) - .setValueAsString(splitTypes.stream().sorted().collect(joining(","))); - } - // TODO(b/199376532): Remove once the system attribute is fully rolled out. + public ManifestEditor setSplitTypes(ImmutableList splitTypes) { manifestElement - .getOrCreateAttribute(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .getOrCreateAndroidAttribute(SPLIT_TYPES_ATTRIBUTE_NAME, SPLIT_TYPES_RESOURCE_ID) .setValueAsString(splitTypes.stream().sorted().collect(joining(","))); return this; } @@ -355,17 +342,10 @@ public ManifestEditor setSplitTypes( * provided by {@code splitTypes} in splits. */ @CanIgnoreReturnValue - public ManifestEditor setRequiredSplitTypes( - ImmutableList splitTypes, boolean enableSystemAttribute) { - if (enableSystemAttribute) { - manifestElement - .getOrCreateAndroidAttribute( - REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID) - .setValueAsString(splitTypes.stream().sorted().collect(joining(","))); - } - // TODO(b/199376532): Remove once the system attribute is fully rolled out. + public ManifestEditor setRequiredSplitTypes(ImmutableList splitTypes) { manifestElement - .getOrCreateAttribute(DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .getOrCreateAndroidAttribute( + REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID) .setValueAsString(splitTypes.stream().sorted().collect(joining(","))); return this; } diff --git a/src/main/java/com/android/tools/build/bundletool/model/ModuleConditions.java b/src/main/java/com/android/tools/build/bundletool/model/ModuleConditions.java index 00f74e0b..9abbfb37 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/ModuleConditions.java +++ b/src/main/java/com/android/tools/build/bundletool/model/ModuleConditions.java @@ -18,9 +18,7 @@ import com.android.bundle.Targeting.DeviceFeature; import com.android.bundle.Targeting.DeviceFeatureTargeting; -import com.android.bundle.Targeting.DeviceGroupModuleTargeting; import com.android.bundle.Targeting.ModuleTargeting; -import com.android.bundle.Targeting.UserCountriesTargeting; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.utils.TargetingProtoUtils; import com.google.auto.value.AutoValue; @@ -87,18 +85,11 @@ public ModuleTargeting toTargeting() { if (getUserCountriesCondition().isPresent()) { UserCountriesCondition condition = getUserCountriesCondition().get(); - moduleTargeting.setUserCountriesTargeting( - UserCountriesTargeting.newBuilder() - .addAllCountryCodes(condition.getCountries()) - .setExclude(condition.getExclude()) - .build()); + moduleTargeting.setUserCountriesTargeting(condition.toTargeting()); } if (getDeviceGroupsCondition().isPresent()) { - moduleTargeting.setDeviceGroupTargeting( - DeviceGroupModuleTargeting.newBuilder() - .addAllValue(getDeviceGroupsCondition().get().getDeviceGroups()) - .build()); + moduleTargeting.setDeviceGroupTargeting(getDeviceGroupsCondition().get().toTargeting()); } return moduleTargeting.build(); diff --git a/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java b/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java index 39c34ed2..b784073d 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java +++ b/src/main/java/com/android/tools/build/bundletool/model/OptimizationDimension.java @@ -24,5 +24,6 @@ public enum OptimizationDimension { TEXTURE_COMPRESSION_FORMAT, DEVICE_TIER, COUNTRY_SET, - AI_MODEL_VERSION + AI_MODEL_VERSION, + DEVICE_GROUP } diff --git a/src/main/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjector.java b/src/main/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjector.java index 64bfc20c..c90fa6e3 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjector.java +++ b/src/main/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjector.java @@ -16,7 +16,6 @@ package com.android.tools.build.bundletool.model; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_T_API_VERSION; import static com.google.common.collect.ImmutableList.toImmutableList; import com.android.bundle.Targeting.ApkTargeting; @@ -35,31 +34,23 @@ public class RequiredSplitTypesInjector { */ @CheckReturnValue public static ImmutableList injectSplitTypeValidation( - ImmutableList splits, - ImmutableList requiredModules, - boolean enableSystemAttribute) { + ImmutableList splits, ImmutableList requiredModules) { return splits.stream() .map( split -> { - // During the validation rollout, only inject system split types attribute for splits - // targeting T+. - boolean includeSystemAttribute = enableSystemAttribute && isTargetingAtLeastT(split); - ManifestEditor apkManifest = split.getAndroidManifest().toEditor(); apkManifest.setSplitTypes( getProvidedSplitTypes(split).stream() .map(RequiredSplitTypeName::toAttributeValue) - .collect(toImmutableList()), - includeSystemAttribute); + .collect(toImmutableList())); // Only base/feature modules have required split types. if (split.isMasterSplit()) { apkManifest.setRequiredSplitTypes( getRequiredSplitTypes(splits, requiredModules, split).stream() .map(RequiredSplitTypeName::toAttributeValue) - .collect(toImmutableList()), - includeSystemAttribute); + .collect(toImmutableList())); } return split.toBuilder().setAndroidManifest(apkManifest.save()).build(); @@ -139,14 +130,6 @@ private static ImmutableSet getRequiredSplitTypes( return splitTypes.build(); } - private static boolean isTargetingAtLeastT(ModuleSplit split) { - return split.getVariantTargeting().getSdkVersionTargeting().getValueList().stream() - .mapToInt(sdkVersion -> sdkVersion.getMin().getValue()) - .min() - .orElse(1) - >= ANDROID_T_API_VERSION; - } - private RequiredSplitTypesInjector() {} static enum RequiredSplitType { diff --git a/src/main/java/com/android/tools/build/bundletool/model/UserCountriesCondition.java b/src/main/java/com/android/tools/build/bundletool/model/UserCountriesCondition.java index 431f5fbb..0679527c 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/UserCountriesCondition.java +++ b/src/main/java/com/android/tools/build/bundletool/model/UserCountriesCondition.java @@ -16,6 +16,7 @@ package com.android.tools.build.bundletool.model; +import com.android.bundle.Targeting.UserCountriesTargeting; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; @@ -39,4 +40,11 @@ public static UserCountriesCondition create( ImmutableList countryCodeList, boolean exclude) { return new AutoValue_UserCountriesCondition(countryCodeList, exclude); } + + public UserCountriesTargeting toTargeting() { + return UserCountriesTargeting.newBuilder() + .addAllCountryCodes(getCountries()) + .setExclude(getExclude()) + .build(); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Provider.java b/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Provider.java index 390328b8..b8159a28 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Provider.java +++ b/src/main/java/com/android/tools/build/bundletool/model/manifestelements/Provider.java @@ -24,11 +24,13 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.NAME_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.PROVIDER_ELEMENT_NAME; +import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElementBuilder; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.Immutable; import java.util.Optional; @@ -47,8 +49,10 @@ public abstract class Provider { abstract Optional> getAuthorities(); + abstract ImmutableList getExtraAttributes(); + public static Builder builder() { - return new AutoValue_Provider.Builder(); + return new AutoValue_Provider.Builder().setExtraAttributes(ImmutableList.of()); } @Memoized @@ -57,6 +61,7 @@ public XmlProtoElement asXmlProtoElement() { setNameAttribute(elementBuilder); setExportedAttribute(elementBuilder); setAuthoritiesAttribute(elementBuilder); + addExtraAttributes(elementBuilder); return elementBuilder.build(); } @@ -84,6 +89,11 @@ private void setAuthoritiesAttribute(XmlProtoElementBuilder elementBuilder) { } } + @CanIgnoreReturnValue + private XmlProtoElementBuilder addExtraAttributes(XmlProtoElementBuilder elementBuilder) { + return elementBuilder.addAllAttribute(getExtraAttributes()); + } + /** Builder for Activity. */ @AutoValue.Builder public abstract static class Builder { @@ -93,6 +103,8 @@ public abstract static class Builder { public abstract Builder setAuthorities(ImmutableList authorities); + public abstract Builder setExtraAttributes(ImmutableList attributes); + public abstract Provider build(); } } diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java index 5f9edf48..b7e356e5 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/TargetingNormalizer.java @@ -26,6 +26,7 @@ import com.android.bundle.Targeting.ApkTargeting; import com.android.bundle.Targeting.AssetsDirectoryTargeting; import com.android.bundle.Targeting.CountrySetTargeting; +import com.android.bundle.Targeting.DeviceGroupTargeting; import com.android.bundle.Targeting.DeviceTierTargeting; import com.android.bundle.Targeting.LanguageTargeting; import com.android.bundle.Targeting.MultiAbi; @@ -99,6 +100,10 @@ public static ApkTargeting normalizeApkTargeting(ApkTargeting targeting) { normalized.setCountrySetTargeting( normalizeCountrySetTargeting(targeting.getCountrySetTargeting())); } + if (targeting.hasDeviceGroupTargeting()) { + normalized.setDeviceGroupTargeting( + normalizeDeviceGroupTargeting(targeting.getDeviceGroupTargeting())); + } return normalized.build(); } @@ -145,6 +150,9 @@ public static AssetsDirectoryTargeting normalizeAssetsDirectoryTargeting( normalized.setTextureCompressionFormat( normalizeTextureCompressionFormatTargeting(targeting.getTextureCompressionFormat())); } + if (targeting.hasDeviceGroup()) { + normalized.setDeviceGroup(normalizeDeviceGroupTargeting(targeting.getDeviceGroup())); + } return normalized.build(); } @@ -234,6 +242,14 @@ private static CountrySetTargeting normalizeCountrySetTargeting(CountrySetTarget .build(); } + private static DeviceGroupTargeting normalizeDeviceGroupTargeting( + DeviceGroupTargeting targeting) { + return DeviceGroupTargeting.newBuilder() + .addAllValue(sortedCopyOf(targeting.getValueList())) + .addAllAlternatives(sortedCopyOf(targeting.getAlternativesList())) + .build(); + } + private static ImmutableList sortInt32Values(Collection values) { return values.stream() .map(Int32Value::getValue) diff --git a/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java b/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java index 3d7698d0..72255e04 100644 --- a/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java +++ b/src/main/java/com/android/tools/build/bundletool/model/utils/ZipUtils.java @@ -22,6 +22,7 @@ import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; import com.google.common.io.ByteSource; import java.io.IOException; import java.io.InputStream; @@ -30,6 +31,7 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; /** Misc utilities for working with zip files. */ public final class ZipUtils { @@ -38,6 +40,20 @@ public static Stream allFileEntriesPaths(ZipFile zipFile) { return allFileEntries(zipFile).map(zipEntry -> ZipPath.create(zipEntry.getName())); } + public static ImmutableList allFileEntriesPaths(ZipInputStream zipInputStream) { + ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); + try { + ZipEntry zipEntry; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + listBuilder.add(ZipPath.create(zipEntry.getName())); + } + } catch (IOException e) { + throw new UncheckedIOException( + String.format("Error reading zip file '%s'.", zipInputStream), e); + } + return listBuilder.build(); + } + public static Stream allFileEntries(ZipFile zipFile) { return zipFile.stream().filter(not(ZipEntry::isDirectory)); } 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 805bfd94..466e779f 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.17.1"; + private static final String CURRENT_VERSION = "1.17.2"; /** Returns the version of BundleTool being run. */ diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java index 6a47e051..098a23db 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/ApkGenerationConfiguration.java @@ -94,8 +94,6 @@ public int getMinimalSdkTargetingForUncompressedDex() { */ public abstract Optional getMinSdkForAdditionalVariantWithV3Rotation(); - public abstract boolean getEnableRequiredSplitTypes(); - public abstract Builder toBuilder(); public static Builder builder() { @@ -112,8 +110,7 @@ public static Builder builder() { .setMasterPinnedResourceIds(ImmutableSet.of()) .setMasterPinnedResourceNames(ImmutableSet.of()) .setBaseManifestReachableResources(ImmutableSet.of()) - .setSuffixStrippings(ImmutableMap.of()) - .setEnableRequiredSplitTypes(false); + .setSuffixStrippings(ImmutableMap.of()); } public static ApkGenerationConfiguration getDefaultInstance() { @@ -158,8 +155,6 @@ public abstract Builder setMinSdkForAdditionalVariantWithV3Rotation( public abstract Builder setEnableBaseModuleMinSdkAsDefaultTargeting( boolean enableBaseModuleMinSdkAsDefaultTargeting); - public abstract Builder setEnableRequiredSplitTypes(boolean enableRequiredSplitTypes); - public abstract ApkGenerationConfiguration build(); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/PerModuleVariantTargetingGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/PerModuleVariantTargetingGenerator.java index 20e86f0f..be9f73db 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/PerModuleVariantTargetingGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/PerModuleVariantTargetingGenerator.java @@ -74,7 +74,6 @@ private static ImmutableList getVariantGenerators( unused -> Stream.of(lPlusVariantTargeting()), new NativeLibsCompressionVariantGenerator(apkGenerationConfiguration), new DexCompressionVariantGenerator(apkGenerationConfiguration), - new RequiredSplitTypesVariantGenerator(apkGenerationConfiguration), new SigningConfigurationVariantGenerator(apkGenerationConfiguration), new SparseEncodingVariantGenerator(apkGenerationConfiguration)); } diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/RequiredSplitTypesVariantGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/RequiredSplitTypesVariantGenerator.java deleted file mode 100644 index 592e7e3f..00000000 --- a/src/main/java/com/android/tools/build/bundletool/splitters/RequiredSplitTypesVariantGenerator.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tools.build.bundletool.splitters; - -import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkVersionFrom; -import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.sdkVersionTargeting; -import static com.android.tools.build.bundletool.model.utils.TargetingProtoUtils.variantTargeting; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_T_API_VERSION; - -import com.android.bundle.Targeting.VariantTargeting; -import com.android.tools.build.bundletool.model.BundleModule; -import java.util.stream.Stream; - -/** Generates variant targetings based on inclusion of required split types attributes. */ -public final class RequiredSplitTypesVariantGenerator implements BundleModuleVariantGenerator { - - private final ApkGenerationConfiguration apkGenerationConfiguration; - - public RequiredSplitTypesVariantGenerator(ApkGenerationConfiguration apkGenerationConfiguration) { - this.apkGenerationConfiguration = apkGenerationConfiguration; - } - - @Override - public Stream generate(BundleModule module) { - if (!apkGenerationConfiguration.getEnableRequiredSplitTypes()) { - return Stream.of(); - } - - return Stream.of(variantTargeting(sdkVersionTargeting(sdkVersionFrom(ANDROID_T_API_VERSION)))); - } -} diff --git a/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java b/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java index 0d415879..e9a79dc4 100644 --- a/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java +++ b/src/main/java/com/android/tools/build/bundletool/splitters/SplitApksGenerator.java @@ -109,10 +109,8 @@ private ImmutableList generateSplitApks( .map(BundleModule::getName) .collect(toImmutableList()); - // Feature flag for enabling the system validation on T+. Remove after b/199376532. - boolean enableSystemAttribute = commonApkGenerationConfiguration.getEnableRequiredSplitTypes(); return RequiredSplitTypesInjector.injectSplitTypeValidation( - splits.build(), nonRemovableModules, enableSystemAttribute); + splits.build(), nonRemovableModules); } private ImmutableList getModulesForVariant( diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java index a2c5a72c..8912d588 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AndroidManifestValidator.java @@ -58,6 +58,7 @@ public void validateAllModules(ImmutableList modules) { validateTargetSandboxVersion(modules); validateTcfTargetingNotMixedWithSupportsGlTexture(modules); validateConditionalModulesAreRemovable(modules); + validateSharedUserId(modules); if (!BundleValidationUtils.isAssetOnlyBundle(modules)) { validateInstant(modules); validateMinSdk(modules); @@ -205,6 +206,16 @@ private static void validateConditionalModulesAreRemovable(ImmutableList modules) { + if (modules.stream().anyMatch(module -> module.getAndroidManifest().hasSharedUserId()) + && modules.stream().anyMatch(module -> module.getRuntimeEnabledSdkConfig().isPresent())) { + throw InvalidBundleException.builder() + .withUserMessage("'sharedUserId' cannot be used with runtime-enabled SDKs.") + .build(); + } + } + @Override public void validateModule(BundleModule module) { validateInstant(module); diff --git a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java index 0e654dd7..b1202ba5 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/AppBundleValidator.java @@ -17,8 +17,10 @@ package com.android.tools.build.bundletool.validation; import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModule; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.zip.ZipFile; /** Validates the files and configuration for the bundle. */ @@ -63,6 +65,7 @@ public class AppBundleValidator { new AssetModuleFilesValidator(), new CodeTransparencyValidator(), new RuntimeEnabledSdkConfigValidator(), + new RuntimeEnabledSdkManifestCompatibilityValidator(), new DeclarativeWatchFaceBundleValidator(), new StandaloneFeatureModulesValidator()); @@ -95,7 +98,7 @@ public static AppBundleValidator create(ImmutableList extraSubVali } /** - * Validates the given App Bundle zip file. + * Validates the given app bundle zip file. * *

Note that this method performs different checks than {@link #validate(AppBundle)}. */ @@ -104,7 +107,7 @@ public void validateFile(ZipFile bundleFile) { } /** - * Validates the given App Bundle. + * Validates the given app bundle. * *

Note that this method performs different checks than {@link #validateFile(ZipFile)}. * @@ -113,4 +116,12 @@ public void validateFile(ZipFile bundleFile) { public void validate(AppBundle bundle) { new ValidatorRunner(allBundleSubValidators).validateBundle(bundle); } + + /** + * Validates the given app bundle in combination with the SDK bundles/archives the app depends on. + */ + public void validateBundleWithSdkModules( + AppBundle bundle, ImmutableMap sdkModules) { + new ValidatorRunner(allBundleSubValidators).validateBundleWithSdkModules(bundle, sdkModules); + } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkManifestCompatibilityValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkManifestCompatibilityValidator.java new file mode 100644 index 00000000..3d9a9048 --- /dev/null +++ b/src/main/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkManifestCompatibilityValidator.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tools.build.bundletool.validation; + +import com.android.tools.build.bundletool.model.AndroidManifest; +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.google.common.collect.ImmutableMap; + +/** Validates that the app manifest is compatible with the runtime-enabled SDKs it depends on. */ +public final class RuntimeEnabledSdkManifestCompatibilityValidator extends SubValidator { + + /** + * Validates that the app manifest is compatible with the manifest of the runtime-enabled SDKs it + * depends on. + */ + @Override + public void validateBundleWithSdkModules( + AppBundle bundle, ImmutableMap sdkModules) { + validateMinSdkVersionBetweeenAppAndSdks(bundle, sdkModules); + validateMinAndTargetSdkVersionAcrossSdks(sdkModules); + } + + /** + * Checks that the minSdkVersion of the app is higher or equal to the minSdkVersion of all + * dependent runtime-enabled SDKs. + */ + private static void validateMinSdkVersionBetweeenAppAndSdks( + AppBundle bundle, ImmutableMap sdkModules) { + int baseMinSdk = + bundle.getModules().values().stream() + .filter(BundleModule::isBaseModule) + .map(BundleModule::getAndroidManifest) + .mapToInt(AndroidManifest::getEffectiveMinSdkVersion) + .findFirst() + .orElseThrow(BundleValidationUtils::createNoBaseModuleException); + + sdkModules + .entrySet() + .forEach( + sdkModule -> { + if (sdkModule.getValue().getAndroidManifest().getEffectiveMinSdkVersion() + > baseMinSdk) { + throw InvalidBundleException.builder() + .withUserMessage( + "Runtime-enabled SDKs must have a minSdkVersion lower than the app, but" + + " found SDK '%s' with minSdkVersion (%d) higher than the app's" + + " minSdkVersion (%d).", + sdkModule.getKey(), + sdkModule.getValue().getAndroidManifest().getEffectiveMinSdkVersion(), + baseMinSdk) + .build(); + } + }); + } + + /** + * Checks that the minSdkVersion of an SDK is never higher than the targetSdkVersion of another + * SDK. + */ + private static void validateMinAndTargetSdkVersionAcrossSdks( + ImmutableMap sdkModules) { + sdkModules + .entrySet() + .forEach( + nameToModule1 -> { + AndroidManifest manifest1 = nameToModule1.getValue().getAndroidManifest(); + sdkModules + .entrySet() + .forEach( + nameToModule2 -> { + AndroidManifest manifest2 = nameToModule2.getValue().getAndroidManifest(); + if (manifest1.getEffectiveMinSdkVersion() + > manifest2.getEffectiveTargetSdkVersion()) { + throw InvalidBundleException.builder() + .withUserMessage( + "Runtime-enabled SDKs must have a minSdkVersion lower or equal to" + + " the targetSdkVersion of another SDK, but found SDK '%s'" + + " with minSdkVersion (%d) higher than the targetSdkVersion" + + " (%d) of SDK '%s'.", + nameToModule1.getKey(), + manifest1.getEffectiveMinSdkVersion(), + manifest2.getEffectiveTargetSdkVersion(), + nameToModule2.getKey()) + .build(); + } + }); + }); + } +} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java index 05f4133f..ab8df203 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidator.java @@ -46,6 +46,7 @@ public void validateModule(BundleModule module) { validateNoSharedUserId(manifest); validateNoComponents(manifest); validateNoSplitId(manifest); + validateTargetSdkVersion(manifest); } private void validateNoSdkLibraryElement(AndroidManifest manifest) { @@ -133,4 +134,12 @@ private void validateNoSplitId(AndroidManifest manifest) { .build(); } } + + private static void validateTargetSdkVersion(AndroidManifest manifest) { + if (!manifest.getTargetSdkVersion().isPresent() || manifest.getTargetSdkVersion().get() < 34) { + throw InvalidBundleException.builder() + .withUserMessage("The 'targetSdkVersion' of an SDK bundle should be 34 or higher.") + .build(); + } + } } diff --git a/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java b/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java index 8cae1d0c..959a6d0f 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/SubValidator.java @@ -21,6 +21,7 @@ import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -32,17 +33,17 @@ */ public abstract class SubValidator { - // Validations of the App Bundle module zip file. + // Validations of the app bundle module zip file. public void validateModuleZipFile(ZipFile moduleFile) {} - // Validations of the Bundle zip file. + // Validations of the bundle zip file. public void validateBundleZipFile(ZipFile bundleFile) {} public void validateBundleZipEntry(ZipFile bundleFile, ZipEntry zipEntry) {} - /** Validates the given SDK Modules zip file. */ + /** Validates the given SDK modules zip file. */ public void validateSdkModulesZipFile(ZipFile modulesFile) {} // Validations of the AppBundle object and its internals. @@ -50,6 +51,12 @@ public void validateSdkModulesZipFile(ZipFile modulesFile) {} /** Validates an AppBundle object. */ public void validateBundle(AppBundle bundle) {} + /** + * Validates an AppBundle object in combination with the SDK bundles/archives the app depends on. + */ + public void validateBundleWithSdkModules( + AppBundle bundle, ImmutableMap sdkModules) {} + public void validateAllModules(ImmutableList modules) {} public void validateModule(BundleModule module) {} diff --git a/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java b/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java index 2ebca895..14cf6fb1 100644 --- a/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java +++ b/src/main/java/com/android/tools/build/bundletool/validation/ValidatorRunner.java @@ -24,6 +24,7 @@ import com.android.tools.build.bundletool.model.SdkBundle; import com.android.tools.build.bundletool.model.ZipPath; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.util.Enumeration; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -37,7 +38,7 @@ public ValidatorRunner(ImmutableList subValidators) { this.subValidators = subValidators; } - /** Validates the given App Bundle zip file. */ + /** Validates the given app bundle zip file. */ public void validateBundleZipFile(ZipFile bundleFile) { subValidators.forEach(subValidator -> subValidator.validateBundleZipFile(bundleFile)); @@ -49,7 +50,7 @@ public void validateBundleZipFile(ZipFile bundleFile) { } } - /** Validates the given App Bundle module zip file. */ + /** Validates the given app bundle module zip file. */ public void validateModuleZipFile(ZipFile moduleFile) { subValidators.forEach(subValidator -> subValidator.validateModuleZipFile(moduleFile)); } @@ -59,12 +60,25 @@ public void validateSdkModulesZipFile(ZipFile moduleFile) { subValidators.forEach(subValidator -> subValidator.validateSdkModulesZipFile(moduleFile)); } - /** Validates the given App Bundle. */ + /** Validates the given app bundle. */ public void validateBundle(AppBundle bundle) { subValidators.forEach(subValidator -> validateBundleUsingSubValidator(bundle, subValidator)); } - /** Validates the given SDK Bundle. */ + /** + * Validates the given app bundle in combination with the SDK bundles/archives the app depends on. + */ + public void validateBundleWithSdkModules( + AppBundle bundle, ImmutableMap sdkModules) { + if (sdkModules.isEmpty()) { + return; + } + + subValidators.forEach( + subValidator -> subValidator.validateBundleWithSdkModules(bundle, sdkModules)); + } + + /** Validates the given SDK bundle. */ void validateSdkBundle(SdkBundle bundle) { subValidators.forEach(subValidator -> validateSdkBundleUsingSubValidator(bundle, subValidator)); } diff --git a/src/main/proto/commands.proto b/src/main/proto/commands.proto index a7ac61eb..9d9c95f0 100644 --- a/src/main/proto/commands.proto +++ b/src/main/proto/commands.proto @@ -194,6 +194,13 @@ message AssetModuleMetadata { // Type of asset module. AssetModuleType asset_module_type = 5; + + // Used for conditional delivery of asset modules which controls the overall + // delivery of asset modules as a single boolean. + // Note that this is different from the AssetsDirectoryTargeting on the asset + // slice level. That targeting decides which slice/variant of the asset module + // to serve. + AssetModuleTargeting targeting = 6; } message InstantMetadata { @@ -227,6 +234,9 @@ enum AssetModuleType { AI_PACK_TYPE = 2; } +// This message name is misleading, as it's not exclusively used to describe +// APKs. It's also used to describe slices of asset modules, which aren't always +// APKs. message ApkDescription { ApkTargeting targeting = 1; diff --git a/src/main/proto/config.proto b/src/main/proto/config.proto index 5f955a07..1ff3b1d8 100644 --- a/src/main/proto/config.proto +++ b/src/main/proto/config.proto @@ -281,6 +281,7 @@ message SplitDimension { DEVICE_TIER = 6; COUNTRY_SET = 7; AI_MODEL_VERSION = 8; + DEVICE_GROUP = 9; } Value value = 1; diff --git a/src/main/proto/device_targeting_config.proto b/src/main/proto/device_targeting_config.proto index ea54b3f6..0de035f8 100644 --- a/src/main/proto/device_targeting_config.proto +++ b/src/main/proto/device_targeting_config.proto @@ -113,6 +113,10 @@ message DeviceSelector { // A device that has any of these system features is excluded by // this selector, even if it matches all other conditions. repeated SystemFeature forbidden_system_features = 5; + + // The SoCs included by this selector. + // Only works for Android S+ devices. + repeated SystemOnChip system_on_chips = 6; } // Conditions about a device's RAM capabilities. @@ -139,6 +143,22 @@ message SystemFeature { string name = 1; } +// Representation of a System-on-Chip (SoC) of an Android device. +// Can be used to target S+ devices. +message SystemOnChip { + // The designer of the SoC, eg. "Google" + // Value of build property "ro.soc.manufacturer" + // https://developer.android.com/reference/android/os/Build#SOC_MANUFACTURER + // Required. + string manufacturer = 1; + + // The model of the SoC, eg. "Tensor" + // Value of build property "ro.soc.model" + // https://developer.android.com/reference/android/os/Build#SOC_MODEL + // Required. + string model = 2; +} + // Properties of a particular device. message DeviceProperties { // Device RAM in bytes. diff --git a/src/main/proto/targeting.proto b/src/main/proto/targeting.proto index 9a53c01c..62e8312f 100644 --- a/src/main/proto/targeting.proto +++ b/src/main/proto/targeting.proto @@ -26,11 +26,15 @@ message ApkTargeting { TextureCompressionFormatTargeting texture_compression_format_targeting = 6; MultiAbiTargeting multi_abi_targeting = 7; SanitizerTargeting sanitizer_targeting = 8; - DeviceTierTargeting device_tier_targeting = 9; - CountrySetTargeting country_set_targeting = 10; + // TODO: b/372902482 - Prefer device_group_targeting. + DeviceTierTargeting device_tier_targeting = 9 [deprecated = true]; + // TODO: b/372902482 - remove. + CountrySetTargeting country_set_targeting = 10 [deprecated = true]; + DeviceGroupTargeting device_group_targeting = 11; } // Targeting on the module level. +// Used for conditional feature modules. // The semantic of the targeting is the "AND" rule on all immediate values. message ModuleTargeting { SdkVersionTargeting sdk_version_targeting = 1; @@ -41,6 +45,15 @@ message ModuleTargeting { reserved 4; } +// Targeting for conditionally delivered AssetModules. +// It's not used for variant-based targeting of AssetModules, see +// AssetsDirectoryTargeting instead. +// The semantic of the targeting is the "AND" rule on all immediate values. +message AssetModuleTargeting { + UserCountriesTargeting user_countries_targeting = 1; + DeviceGroupModuleTargeting device_group_targeting = 2; +} + // User Countries targeting describing an inclusive/exclusive list of country // codes that module targets. message UserCountriesTargeting { @@ -131,8 +144,11 @@ message AssetsDirectoryTargeting { reserved 2; // was GraphicsApiTargeting TextureCompressionFormatTargeting texture_compression_format = 3; LanguageTargeting language = 4; - DeviceTierTargeting device_tier = 5; - CountrySetTargeting country_set = 6; + // TODO: b/372902482 - Prefer device_group. + DeviceTierTargeting device_tier = 5 [deprecated = true]; + // TODO: b/372902482 - remove. + CountrySetTargeting country_set = 6 [deprecated = true]; + DeviceGroupTargeting device_group = 7; } // Targeting specific for directories under lib/. @@ -235,6 +251,14 @@ message CountrySetTargeting { repeated string alternatives = 2; } +// Targets assets and APKs to a concrete device group. +message DeviceGroupTargeting { + // Device group name defined in device tier config. + repeated string value = 1; + // Targeting of other sibling directories that are in the Bundle. + repeated string alternatives = 2; +} + // Targets conditional modules to a set of device groups. message DeviceGroupModuleTargeting { repeated string value = 1; diff --git a/src/test/java/com/android/tools/build/bundletool/androidtools/DefaultCommandExecutorTest.java b/src/test/java/com/android/tools/build/bundletool/androidtools/DefaultCommandExecutorTest.java new file mode 100644 index 00000000..0b53cf1a --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/androidtools/DefaultCommandExecutorTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 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.androidtools; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.android.tools.build.bundletool.androidtools.CommandExecutor.CommandOptions; +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class DefaultCommandExecutorTest { + + private static File createTempFile(String fileName, String content) throws IOException { + File file = File.createTempFile(fileName, null); + try (PrintWriter writer = new PrintWriter(file, UTF_8)) { + writer.println(content); + } + return file; + } + + @Test + public void executeAndCapture_capturesOutput() { + DefaultCommandExecutor commandExecutor = new DefaultCommandExecutor(); + ImmutableList command = ImmutableList.of("echo", "Hello World"); + CommandOptions options = CommandOptions.builder().setTimeout(Duration.ofSeconds(1)).build(); + ImmutableList output = commandExecutor.executeAndCapture(command, options); + assertThat(output).containsExactly("Hello World"); + } + + @Test + public void executeAndCapture_capturesLongOutput() throws IOException { + String longString = new String(new char[1024 * 1024]).replace('\0', 'a'); + File f = createTempFile("long_output.txt", longString); + DefaultCommandExecutor commandExecutor = new DefaultCommandExecutor(); + ImmutableList command = ImmutableList.of("cat", f.getAbsolutePath()); + CommandOptions options = CommandOptions.builder().setTimeout(Duration.ofSeconds(1)).build(); + ImmutableList output = commandExecutor.executeAndCapture(command, options); + assertThat(output).containsExactly(longString); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java index 6d88a14c..0cb2d0ce 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksCommandTest.java @@ -27,7 +27,6 @@ import static com.android.tools.build.bundletool.model.OptimizationDimension.SCREEN_DENSITY; import static com.android.tools.build.bundletool.model.OptimizationDimension.TEXTURE_COMPRESSION_FORMAT; import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; import static com.android.tools.build.bundletool.testing.Aapt2Helper.AAPT2_PATH; import static com.android.tools.build.bundletool.testing.DeviceFactory.abis; import static com.android.tools.build.bundletool.testing.DeviceFactory.createDeviceSpecFile; @@ -39,6 +38,7 @@ import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_SERIAL; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSdkVersion; import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.createSdkModulesConfig; import static com.android.tools.build.bundletool.testing.TestUtils.addKeyToKeystore; import static com.android.tools.build.bundletool.testing.TestUtils.createDebugKeystore; @@ -1537,7 +1537,6 @@ public void buildingViaFlagsAndBuilderHasSameResult_customStorePackage() throws assertThat(commandViaBuilder.build()).isEqualTo(commandViaFlags); } - @Test public void missingBundleFile_throws() throws Exception { Path bundlePath = tmpDir.resolve("bundle.aab"); @@ -2744,6 +2743,65 @@ public void validateRuntimeEnabledSdkConfig_missingRequiredField_throws() throws .contains("Found dependency on runtime-enabled SDK with an empty package name."); } + @Test + public void appBundleHasSdkDeps_badSdkMinSdkVersion_runsBundleWithSdkModulesValidations_throws() + throws Exception { + AppBundleBuilder appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module + .setManifest(androidManifest("com.app", withMinSdkVersion(31))) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk1") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest(VALID_CERT_FINGERPRINT) + .setResourcesPackageId(2)) + .build()) + .build()); + createAppBundle(bundlePath, appBundle.build()); + + new SdkBundleSerializer() + .writeToDisk( + new SdkBundleBuilder() + .setSdkModulesConfig( + createSdkModulesConfig() + .setSdkPackageName("com.test.sdk1") + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder().setMajor(1).setMinor(2)) + .build()) + .setModule( + new BundleModuleBuilder("base") + .setManifest( + androidManifest( + "com.test.sdk1", withMinSdkVersion(32), withTargetSdkVersion(34))) + .build()) + .build(), + sdkBundlePath1); + + BuildApksCommand command = + BuildApksCommand.fromFlags( + new FlagParser() + .parse( + "--bundle=" + bundlePath, + "--output=" + outputFilePath, + "--sdk-bundles=" + sdkBundlePath1), + fakeAdbServer); + + Exception e = assertThrows(InvalidBundleException.class, command::execute); + assertThat(e) + .hasMessageThat() + .contains( + "Runtime-enabled SDKs must have a minSdkVersion lower than the app, but found SDK" + + " 'com.test.sdk1' with minSdkVersion (32) higher than the app's minSdkVersion" + + " (31)."); + } + @Test public void buildApks_fromAppBundleWithRuntimeEnabledSdkDeps_succeeds() throws Exception { createSdkBundle(sdkBundlePath1, "com.test.sdk1", /* majorVersion= */ 1, /* minorVersion= */ 2); @@ -2783,7 +2841,7 @@ public void buildApks_fromAppBundleWithRuntimeEnabledSdkDeps_succeeds() throws E Variant nonSdkRuntimeVariant = buildApksResult.getVariant(0); assertThat(nonSdkRuntimeVariant.getTargeting()) - .isEqualTo(TargetingUtils.variantSdkTargeting(ANDROID_L_API_VERSION)); + .isEqualTo(TargetingUtils.variantSdkTargeting(33)); // non-sdk-runtime variant contains additional modules - one per SDK dependency. assertThat(nonSdkRuntimeVariant.getApkSetCount()).isEqualTo(3); assertThat( @@ -2869,8 +2927,7 @@ private void createAppBundleWithRuntimeEnabledSdkConfig( "base", module -> module - .setManifest( - androidManifest("com.app", withMinSdkVersion(ANDROID_L_API_VERSION))) + .setManifest(androidManifest("com.app", withMinSdkVersion(33))) .setRuntimeEnabledSdkConfig(runtimeEnabledSdkConfig) .build()); createAppBundle(path, appBundle.build()); 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 e7e01e43..97509c6d 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 @@ -50,7 +50,6 @@ import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_Q_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_S_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_S_V2_API_VERSION; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_T_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_U_API_VERSION; import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractFromApkSetFile; import static com.android.tools.build.bundletool.testing.ApkSetUtils.extractTocFromApkSetFile; @@ -299,7 +298,6 @@ public class BuildApksManagerTest { private static final SdkVersion P_SDK_VERSION = sdkVersionFrom(ANDROID_P_API_VERSION); private static final SdkVersion Q_SDK_VERSION = sdkVersionFrom(ANDROID_Q_API_VERSION); private static final SdkVersion S_SDK_VERSION = sdkVersionFrom(ANDROID_S_API_VERSION); - private static final SdkVersion T_SDK_VERSION = sdkVersionFrom(ANDROID_T_API_VERSION); private static final SdkVersion S2_V2_SDK_VERSION = sdkVersionFrom(ANDROID_S_V2_API_VERSION); private static final int ALIGNMENT_4K = 4096; @@ -328,7 +326,7 @@ public class BuildApksManagerTest { @Inject BuildApksCommand command; protected TestModule.Builder createTestModuleBuilder() { - return TestModule.builder().withEnableRequiredSplitTypes(false); + return TestModule.builder(); } @BeforeClass @@ -2233,7 +2231,6 @@ public void buildApksCommand_splitApks_targetLPlus() throws Exception { .containsExactly(L_SDK_VERSION); } - @Test public void buildApksCommand_splitApks_targetMinSdkVersion() throws Exception { AppBundle appBundle = diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java index 49c20346..c494e55c 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildApksResourcePinningTest.java @@ -161,12 +161,7 @@ public void resourceIds_pinnedToMasterSplits() throws Exception { .build(); TestComponent.useTestModule( - this, - TestModule.builder() - .withEnableRequiredSplitTypes(false) - .withAppBundle(appBundle) - .withOutputPath(outputFilePath) - .build()); + this, TestModule.builder().withAppBundle(appBundle).withOutputPath(outputFilePath).build()); buildApksManager.execute(); ZipFile apkSetFile = new ZipFile(outputFilePath.toFile()); @@ -210,7 +205,7 @@ public void resourceIds_pinnedToMasterSplits() throws Exception { baseModuleApks, apkDescription -> apkDescription.getSplitApkMetadata().getIsMasterSplit()); - ApkDescription baseMaster = apkBaseMaster.get(/* isMasterSplit= */ true); + ApkDescription baseMaster = apkBaseMaster.get(true); File baseMasterFile = extractFromApkSetFile(apkSetFile, baseMaster.getPath(), outputDir); try (ZipFile baseMasterZip = new ZipFile(baseMasterFile)) { assertThat(filesUnderPath(baseMasterZip, ZipPath.create("res"))) @@ -220,7 +215,7 @@ public void resourceIds_pinnedToMasterSplits() throws Exception { "res/drawable/image2.jpg", "res/xml/splits0.xml"); - ApkDescription baseFr = apkBaseMaster.get(/* isMasterSplit= */ false); + ApkDescription baseFr = apkBaseMaster.get(false); File baseFrFile = extractFromApkSetFile(apkSetFile, baseFr.getPath(), outputDir); try (ZipFile baseFrZip = new ZipFile(baseFrFile)) { assertThat(filesUnderPath(baseFrZip, ZipPath.create("res"))) @@ -234,7 +229,7 @@ public void resourceIds_pinnedToMasterSplits() throws Exception { featureModuleApks, apkDescription -> apkDescription.getSplitApkMetadata().getIsMasterSplit()); - ApkDescription featureMaster = apkFeatureMaster.get(/* isMasterSplit= */ true); + ApkDescription featureMaster = apkFeatureMaster.get(true); File featureMasterFile = extractFromApkSetFile(apkSetFile, featureMaster.getPath(), outputDir); try (ZipFile featureMasterZip = new ZipFile(featureMasterFile)) { @@ -245,7 +240,7 @@ public void resourceIds_pinnedToMasterSplits() throws Exception { "res/drawable-fr/image4.jpg"); } - ApkDescription featureFr = apkFeatureMaster.get(/* isMasterSplit= */ false); + ApkDescription featureFr = apkFeatureMaster.get(false); File featureFrFile = extractFromApkSetFile(apkSetFile, featureFr.getPath(), outputDir); try (ZipFile featureFrZip = new ZipFile(featureFrFile)) { assertThat(filesUnderPath(featureFrZip, ZipPath.create("res"))) diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java index 9736a265..27e5c21d 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkApksCommandTest.java @@ -619,7 +619,6 @@ public void verboseIsFalseByDefault() { assertThat(command.getVerbose()).isFalse(); } - private ParsedFlags getDefaultFlagsWithAdditionalFlags(String... additionalFlags) { String[] flags = Stream.concat(getDefaultFlagList().stream(), stream(additionalFlags)) 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 bcea6eba..f7e6c59b 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 @@ -18,7 +18,6 @@ import static com.android.tools.build.bundletool.model.utils.BundleParser.EXTRACTED_SDK_MODULES_FILE_NAME; import static com.android.tools.build.bundletool.model.utils.FileNames.TABLE_OF_CONTENTS_FILE; -import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; import static com.android.tools.build.bundletool.testing.Aapt2Helper.AAPT2_PATH; import static com.android.tools.build.bundletool.testing.FakeSystemEnvironmentProvider.ANDROID_HOME; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; @@ -601,8 +600,7 @@ public void generateModuleSplit_withAsar_sameAsBuildApks() throws Exception { new AppBundleBuilder() .addModule( new BundleModuleBuilder("base") - .setManifest( - androidManifest("com.test.app", withMinSdkVersion(ANDROID_L_API_VERSION))) + .setManifest(androidManifest("com.test.app", withMinSdkVersion(33))) .setRuntimeEnabledSdkConfig( RuntimeEnabledSdkConfig.newBuilder() .addRuntimeEnabledSdk( @@ -655,8 +653,7 @@ public void generateModuleSplit_withSdkBundle_sameAsBuildApks() throws Exception new AppBundleBuilder() .addModule( new BundleModuleBuilder("base") - .setManifest( - androidManifest("com.test.app", withMinSdkVersion(ANDROID_L_API_VERSION))) + .setManifest(androidManifest("com.test.app", withMinSdkVersion(33))) .setRuntimeEnabledSdkConfig( RuntimeEnabledSdkConfig.newBuilder() .addRuntimeEnabledSdk( diff --git a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java index f30e0d0d..34e7b648 100644 --- a/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java +++ b/src/test/java/com/android/tools/build/bundletool/commands/BuildSdkBundleCommandTest.java @@ -19,14 +19,19 @@ import static com.android.bundle.Targeting.Abi.AbiAlias.X86; import static com.android.bundle.Targeting.Abi.AbiAlias.X86_64; import static com.android.tools.build.bundletool.model.AndroidManifest.ACTIVITY_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.ANDROID_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.APPLICATION_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PERMISSION_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.PROVIDER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.RECEIVER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SERVICE_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SDK_VERSION_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.TARGET_SDK_VERSION_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.USES_SDK_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.RuntimeEnabledSdkVersionEncoder.VERSION_MAJOR_MAX_VALUE; -import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifestForSdkBundle; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlDecimalIntegerAttribute; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlElement; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.xmlNode; import static com.android.tools.build.bundletool.testing.TargetingUtils.assetsDirectoryTargeting; @@ -683,7 +688,7 @@ public void invalidSdkModulesConfig_throws() throws Exception { @Test public void validModule() throws Exception { - XmlNode manifest = androidManifest(PKG_NAME); + XmlNode manifest = androidManifestForSdkBundle(PKG_NAME); ResourceTable resourceTable = new ResourceTableBuilder() .addPackage(PKG_NAME) @@ -748,7 +753,7 @@ public void validModule() throws Exception { @Test public void assetsTargeting_generated() throws Exception { - XmlNode manifest = androidManifest(PKG_NAME); + XmlNode manifest = androidManifestForSdkBundle(PKG_NAME); Path module = new ZipBuilder() .addFileWithProtoContent(ZipPath.create("manifest/AndroidManifest.xml"), manifest) @@ -804,7 +809,7 @@ public void assetsTargeting_generated() throws Exception { @Test public void nativeTargeting_generated() throws Exception { - XmlNode manifest = androidManifest(PKG_NAME); + XmlNode manifest = androidManifestForSdkBundle(PKG_NAME); Path module = new ZipBuilder() .addFileWithProtoContent(ZipPath.create("manifest/AndroidManifest.xml"), manifest) @@ -857,7 +862,7 @@ public void nativeTargeting_generated() throws Exception { @Test public void sdkBundleConfig_isSaved() throws Exception { - XmlNode manifest = androidManifest(PKG_NAME); + XmlNode manifest = androidManifestForSdkBundle(PKG_NAME); Path module = new ZipBuilder() .addFileWithProtoContent(ZipPath.create("manifest/AndroidManifest.xml"), manifest) @@ -885,6 +890,14 @@ public void androidManifestSanitized() throws Exception { xmlElement( "manifest", xmlNode(xmlElement(PERMISSION_ELEMENT_NAME)), + xmlNode( + xmlElement( + USES_SDK_ELEMENT_NAME, + xmlDecimalIntegerAttribute( + ANDROID_NAMESPACE_URI, + TARGET_SDK_VERSION_ATTRIBUTE_NAME, + TARGET_SDK_VERSION_RESOURCE_ID, + 34))), xmlNode( xmlElement( APPLICATION_ELEMENT_NAME, @@ -908,7 +921,18 @@ public void androidManifestSanitized() throws Exception { .execute(); XmlNode sanitizedManifest = - xmlNode(xmlElement("manifest", xmlNode(xmlElement(APPLICATION_ELEMENT_NAME)))); + xmlNode( + xmlElement( + "manifest", + xmlNode( + xmlElement( + USES_SDK_ELEMENT_NAME, + xmlDecimalIntegerAttribute( + ANDROID_NAMESPACE_URI, + TARGET_SDK_VERSION_ATTRIBUTE_NAME, + TARGET_SDK_VERSION_RESOURCE_ID, + 34))), + xmlNode(xmlElement(APPLICATION_ELEMENT_NAME)))); try (ZipFile bundle = new ZipFile(bundlePath.toFile())) { ZipEntry modulesEntry = bundle.getEntry("modules.resm"); Path modulesPath = tmpDir.resolve("modules.resm"); @@ -948,7 +972,7 @@ public void overwriteFlagNotSetRejectsCommandIfOutputAlreadyExists() throws Exce private Path createSimpleBaseModule() throws IOException { return new ZipBuilder() .addFileWithProtoContent( - ZipPath.create("manifest/AndroidManifest.xml"), androidManifest(PKG_NAME)) + ZipPath.create("manifest/AndroidManifest.xml"), androidManifestForSdkBundle(PKG_NAME)) .writeTo(tmpDir.resolve("base.zip")); } @@ -966,7 +990,7 @@ private Path buildSimpleModule(String moduleName, String fileName) throws IOExce return new ZipBuilder() .addFileWithProtoContent( ZipPath.create("manifest/AndroidManifest.xml"), - androidManifest(PKG_NAME, manifestMutators)) + androidManifestForSdkBundle(PKG_NAME, manifestMutators)) .writeTo(tmpDir.resolve(fileName + ".zip")); } diff --git a/src/test/java/com/android/tools/build/bundletool/commands/DumpSdkBundleCommandTest.java b/src/test/java/com/android/tools/build/bundletool/commands/DumpSdkBundleCommandTest.java new file mode 100644 index 00000000..bc7b2657 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/DumpSdkBundleCommandTest.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.aapt.Resources.ResourceTable; +import com.android.bundle.Config.BundleConfig.BundleType; +import com.android.bundle.SdkBundleConfigProto.SdkBundleConfig; +import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; +import com.android.tools.build.bundletool.commands.DumpSdkBundleCommand.DumpTarget; +import com.android.tools.build.bundletool.flags.FlagParser; +import com.android.tools.build.bundletool.io.SdkBundleSerializer; +import com.android.tools.build.bundletool.model.BundleMetadata; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException; +import com.android.tools.build.bundletool.model.version.Version; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Before; +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 final class DumpSdkBundleCommandTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private Path bundlePath; + + @Before + public void setUp() { + bundlePath = temporaryFolder.getRoot().toPath().resolve("bundle.asb"); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_defaults() { + DumpSdkBundleCommand commandViaFlags = + DumpSdkBundleCommand.fromFlags( + new FlagParser().parse("dump", "manifest", "--bundle=" + bundlePath)); + + DumpSdkBundleCommand commandViaBuilder = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.MANIFEST) + .setBundlePath(bundlePath) + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_withPrintValues() { + DumpSdkBundleCommand commandViaFlags = + DumpSdkBundleCommand.fromFlags( + new FlagParser().parse("dump", "manifest", "--bundle=" + bundlePath, "--values")); + + DumpSdkBundleCommand commandViaBuilder = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.MANIFEST) + .setBundlePath(bundlePath) + .setPrintValues(true) + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_resourceId() { + DumpSdkBundleCommand commandViaFlags = + DumpSdkBundleCommand.fromFlags( + new FlagParser() + .parse("dump", "manifest", "--bundle=" + bundlePath, "--resource=0x12345678")); + + DumpSdkBundleCommand commandViaBuilder = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.MANIFEST) + .setBundlePath(bundlePath) + .setResourceId(0x12345678) + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_negativeResourceId() { + DumpSdkBundleCommand commandViaFlags = + DumpSdkBundleCommand.fromFlags( + new FlagParser() + .parse("dump", "manifest", "--bundle=" + bundlePath, "--resource=0x80200000")); + + DumpSdkBundleCommand commandViaBuilder = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.MANIFEST) + .setBundlePath(bundlePath) + .setResourceId(0x80200000) + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_resourceName() { + DumpSdkBundleCommand commandViaFlags = + DumpSdkBundleCommand.fromFlags( + new FlagParser() + .parse("dump", "manifest", "--bundle=" + bundlePath, "--resource=drawable/icon")); + + DumpSdkBundleCommand commandViaBuilder = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.MANIFEST) + .setBundlePath(bundlePath) + .setResourceName("drawable/icon") + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_withXPath() { + DumpSdkBundleCommand commandViaFlags = + DumpSdkBundleCommand.fromFlags( + new FlagParser() + .parse( + "dump", + "manifest", + "--bundle=" + bundlePath, + "--xpath=/manifest/@versionCode")); + + DumpSdkBundleCommand commandViaBuilder = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.MANIFEST) + .setBundlePath(bundlePath) + .setXPathExpression("/manifest/@versionCode") + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + @Test + public void buildingViaFlagsAndBuilderHasSameResult_bundleConfig() { + DumpSdkBundleCommand commandViaFlags = + DumpSdkBundleCommand.fromFlags( + new FlagParser().parse("dump", "config", "--bundle=" + bundlePath)); + + DumpSdkBundleCommand commandViaBuilder = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.CONFIG) + .setBundlePath(bundlePath) + .build(); + + assertThat(commandViaBuilder).isEqualTo(commandViaFlags); + } + + @Test + public void dumpFileThatDoesNotExist() { + DumpSdkBundleCommand command = + DumpSdkBundleCommand.builder() + .setDumpTarget(DumpTarget.MANIFEST) + .setBundlePath(Paths.get("/tmp/random-file")) + .build(); + + assertThrows(IllegalArgumentException.class, command::execute); + } + + @Test + public void dumpInvalidTarget() { + InvalidCommandException exception = + assertThrows( + InvalidCommandException.class, + () -> + DumpSdkBundleCommand.fromFlags( + new FlagParser().parse("dump", "blah", "--bundle=" + bundlePath))); + + assertThat(exception) + .hasMessageThat() + .matches("Unrecognized dump target: 'blah'. Accepted values are: .*"); + } + + @Test + public void dumpResources_withXPath_throws() throws Exception { + createBundle(bundlePath); + + DumpSdkBundleCommand dumpSdkBundleCommand = + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.RESOURCES) + .setXPathExpression("/manifest/@nothing-that-exists") + .build(); + InvalidCommandException exception = + assertThrows(InvalidCommandException.class, dumpSdkBundleCommand::execute); + assertThat(exception) + .hasMessageThat() + .contains("Cannot pass an XPath expression when dumping resources."); + } + + @Test + public void dumpResources_resourceIdAndResourceNameSet_throws() throws Exception { + createBundle(bundlePath); + + DumpSdkBundleCommand dumpSdkBundleCommand = + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.RESOURCES) + .setResourceId(0x12345678) + .setResourceName("drawable/icon") + .build(); + InvalidCommandException exception = + assertThrows(InvalidCommandException.class, dumpSdkBundleCommand::execute); + assertThat(exception) + .hasMessageThat() + .contains("Cannot pass both resource ID and resource name."); + } + + @Test + public void dumpManifest_resourceIdSet_throws() throws Exception { + createBundle(bundlePath); + + DumpSdkBundleCommand dumpSdkBundleCommand = + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setResourceId(0x12345678) + .build(); + InvalidCommandException exception = + assertThrows(InvalidCommandException.class, dumpSdkBundleCommand::execute); + assertThat(exception) + .hasMessageThat() + .contains("The resource name/id can only be passed when dumping resources."); + } + + @Test + public void dumpManifest_printValues_throws() throws Exception { + createBundle(bundlePath); + + DumpSdkBundleCommand dumpSdkBundleCommand = + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setPrintValues(true) + .build(); + InvalidCommandException exception = + assertThrows(InvalidCommandException.class, dumpSdkBundleCommand::execute); + assertThat(exception) + .hasMessageThat() + .contains("Printing resource values can only be requested when dumping resources."); + } + + private static void createBundle(Path bundlePath) throws IOException { + createBundleWithResourceTable(bundlePath, ResourceTable.getDefaultInstance()); + } + + private static void createBundleWithResourceTable(Path bundlePath, ResourceTable resourceTable) + throws IOException { + SdkBundle sdkBundle = + SdkBundle.builder() + .setModule( + BundleModule.builder() + .setName(BundleModuleName.BASE_MODULE_NAME) + .setBundleType(BundleType.REGULAR) + .setBundletoolVersion(Version.of("1.1.1")) + .setAndroidManifestProto(androidManifest("com.app")) + .setResourceTable(resourceTable) + .build()) + .setSdkModulesConfig(SdkModulesConfig.getDefaultInstance()) + .setSdkBundleConfig(SdkBundleConfig.getDefaultInstance()) + .setBundleMetadata(BundleMetadata.builder().build()) + .build(); + + new SdkBundleSerializer().writeToDisk(sdkBundle, bundlePath); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/commands/DumpSdkBundleManagerTest.java b/src/test/java/com/android/tools/build/bundletool/commands/DumpSdkBundleManagerTest.java new file mode 100644 index 00000000..84636746 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/commands/DumpSdkBundleManagerTest.java @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.commands; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withDebuggableAttribute; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMetadataValue; +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.aapt.Resources.ResourceTable; +import com.android.aapt.Resources.XmlElement; +import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Config.BundleConfig.BundleType; +import com.android.bundle.Config.Bundletool; +import com.android.bundle.SdkBundleConfigProto.SdkBundleConfig; +import com.android.bundle.SdkModulesConfigOuterClass.RuntimeEnabledSdkVersion; +import com.android.bundle.SdkModulesConfigOuterClass.SdkModulesConfig; +import com.android.tools.build.bundletool.commands.DumpSdkBundleCommand.DumpTarget; +import com.android.tools.build.bundletool.io.SdkBundleSerializer; +import com.android.tools.build.bundletool.model.BundleMetadata; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.BundleModuleName; +import com.android.tools.build.bundletool.model.SdkBundle; +import com.android.tools.build.bundletool.model.version.Version; +import com.android.tools.build.bundletool.testing.ResourceTableBuilder; +import com.google.common.collect.ImmutableSortedMap; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Path; +import org.junit.Before; +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 final class DumpSdkBundleManagerTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private Path bundlePath; + + @Before + public void setUp() { + bundlePath = temporaryFolder.getRoot().toPath().resolve("bundle.asb"); + } + + @Test + public void dumpManifest() throws Exception { + XmlNode manifest = + XmlNode.newBuilder() + .setElement(XmlElement.newBuilder().setName("manifest").build()) + .build(); + createBundle(bundlePath, manifest); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + assertThat(new String(outputStream.toByteArray(), UTF_8)) + .isEqualTo(String.format("%n")); + } + + @Test + public void dumpManifest_withXPath_singleValue() throws Exception { + createBundle(bundlePath); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setXPathExpression("/manifest/@package") + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + assertThat(new String(outputStream.toByteArray(), UTF_8)).isEqualTo(String.format("com.app%n")); + } + + @Test + public void dumpManifest_withXPath_multipleValues() throws Exception { + createBundle( + bundlePath, + androidManifest( + "com.app", withMetadataValue("key1", "value1"), withMetadataValue("key2", "value2"))); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setXPathExpression("/manifest/application/meta-data/@android:value") + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + assertThat(new String(outputStream.toByteArray(), UTF_8)) + .isEqualTo(String.format("value1%n" + "value2%n")); + } + + @Test + public void dumpManifest_withXPath_nodeResult() throws Exception { + createBundle( + bundlePath, + androidManifest( + "com.app", withMetadataValue("key1", "value1"), withMetadataValue("key2", "value2"))); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand dumpCommand = + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setXPathExpression("/manifest/application/meta-data") + .setOutputStream(new PrintStream(outputStream)) + .build(); + + assertThrows(UnsupportedOperationException.class, () -> dumpCommand.execute()); + } + + @Test + public void dumpManifest_withXPath_predicate() throws Exception { + createBundle( + bundlePath, + androidManifest( + "com.app", withMetadataValue("key1", "value1"), withMetadataValue("key2", "value2"))); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setXPathExpression( + "/manifest/application/meta-data[@android:name = \"key2\"]/@android:value") + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + assertThat(new String(outputStream.toByteArray(), UTF_8)).isEqualTo(String.format("value2%n")); + } + + @Test + public void dumpManifest_withXPath_noMatch() throws Exception { + createBundle(bundlePath); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setXPathExpression("/manifest/@nothing-that-exists") + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + assertThat(new String(outputStream.toByteArray(), UTF_8)).isEqualTo(String.format("%n")); + } + + @Test + public void dumpManifest_withXPath_noNamespaceDeclaration() throws Exception { + XmlNode manifestWithoutNamespaceDeclaration = + androidManifest( + "com.app", + withDebuggableAttribute(true), + manifestElement -> manifestElement.getProto().clearNamespaceDeclaration()); + + createBundle(bundlePath, manifestWithoutNamespaceDeclaration); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.MANIFEST) + .setXPathExpression("/manifest/application/@android:debuggable") + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + assertThat(new String(outputStream.toByteArray(), UTF_8).trim()).isEqualTo("true"); + } + + @Test + public void dumpResources_allTable() throws Exception { + createBundle( + bundlePath, + new ResourceTableBuilder() + .addPackage("com.app") + .addStringResourceForMultipleLocales( + "title", ImmutableSortedMap.of("en", "Title", "pt", "Título")) + .addDrawableResourceForMultipleDensities( + "icon", + ImmutableSortedMap.of( + 160, "res/drawable/icon.png", 240, "res/drawable-hdpi/icon.png")) + .build()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.RESOURCES) + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output) + .isEqualTo( + String.format( + "Package 'com.app':%n" + + "0x7f010000 - string/title%n" + + "\tlocale: \"en\"%n" + + "\tlocale: \"pt\"%n" + + "0x7f020000 - drawable/icon%n" + + "\tdensity: 160%n" + + "\tdensity: 240%n%n")); + } + + @Test + public void dumpResources_resourceId() throws Exception { + createBundle( + bundlePath, + new ResourceTableBuilder() + .addPackage("com.app") + .addStringResourceForMultipleLocales( + "title", ImmutableSortedMap.of("en", "Title", "pt", "Título")) + .addDrawableResourceForMultipleDensities( + "icon", + ImmutableSortedMap.of( + 160, "res/drawable/icon.png", 240, "res/drawable-hdpi/icon.png")) + .build()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.RESOURCES) + .setOutputStream(new PrintStream(outputStream)) + .setResourceId(0x7f010000) + .build() + .execute(); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output) + .isEqualTo( + String.format( + "Package 'com.app':%n" + + "0x7f010000 - string/title%n" + + "\tlocale: \"en\"%n" + + "\tlocale: \"pt\"%n%n")); + } + + @Test + public void dumpResources_resourceName() throws Exception { + createBundle( + bundlePath, + new ResourceTableBuilder() + .addPackage("com.app") + .addStringResource("icon", "Icon") + .addMipmapResource("icon", "res/mipmap-hdpi/icon.png") + .addDrawableResource("icon", "res/drawable/icon.png") + .build()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.RESOURCES) + .setOutputStream(new PrintStream(outputStream)) + .setResourceName("string/icon") + .build() + .execute(); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output) + .isEqualTo( + String.format( + "Package 'com.app':%n" + "0x7f010000 - string/icon%n" + "\t(default)%n%n")); + } + + @Test + public void printResources_withValues() throws Exception { + createBundle( + bundlePath, + new ResourceTableBuilder() + .addPackage("com.app") + .addStringResource("title", "Title") + .addDrawableResource("icon", "res/drawable/icon.png") + .build()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.RESOURCES) + .setOutputStream(new PrintStream(outputStream)) + .setPrintValues(true) + .build() + .execute(); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output) + .isEqualTo( + String.format( + "Package 'com.app':%n" + + "0x7f010000 - string/title%n" + + "\t(default) - [STR] \"Title\"%n" + + "0x7f020000 - drawable/icon%n" + + "\t(default) - [FILE] res/drawable/icon.png%n" + + "%n")); + } + + @Test + public void printResources_valuesEscaped() throws Exception { + createBundle( + bundlePath, + new ResourceTableBuilder() + .addPackage("com.app") + .addStringResource("text", "First line\nSecond line\nThird line") + .addStringResource("text2", "First line\r\nSecond line\r\nThird line") + .addStringResource("text3", "First line\u2028Second line\u2028Third line") + .addStringResource("text4", "First line\\nSame line!") + .addStringResource("text5", "Text \"with\" quotes!") + .build()); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.RESOURCES) + .setOutputStream(new PrintStream(outputStream)) + .setPrintValues(true) + .build() + .execute(); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output) + .isEqualTo( + String.format( + "Package 'com.app':%n" + + "0x7f010000 - string/text%n" + + "\t(default) - [STR] \"First line\\nSecond line\\nThird line\"%n" + + "0x7f010001 - string/text2%n" + + "\t(default) - [STR] \"First line\\r\\nSecond line\\r\\nThird line\"%n" + + "0x7f010002 - string/text3%n" + + "\t(default) - [STR] \"First line\\u2028Second line\\u2028Third line\"%n" + + "0x7f010003 - string/text4%n" + + "\t(default) - [STR] \"First line\\\\nSame line!\"%n" + + "0x7f010004 - string/text5%n" + + "\t(default) - [STR] \"Text \\\"with\\\" quotes!\"%n" + + "%n")); + } + + @Test + public void printBundleConfig() throws Exception { + createBundle(bundlePath); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + DumpSdkBundleCommand.builder() + .setBundlePath(bundlePath) + .setDumpTarget(DumpTarget.CONFIG) + .setOutputStream(new PrintStream(outputStream)) + .build() + .execute(); + + String output = new String(outputStream.toByteArray(), UTF_8); + assertThat(output) + .isEqualTo( + String.format( + "{\n" + + " \"bundletool\": {\n" + + " \"version\": \"1.2.3\"\n" + + " },\n" + + " \"sdkPackageName\": \"com.sdk\",\n" + + " \"sdkVersion\": {\n" + + " \"major\": 1,\n" + + " \"minor\": 2,\n" + + " \"patch\": 3\n" + + " },\n" + + " \"sdkProviderClassName\": \"com.sdk.SandboxedSdkProviderAdapter\",\n" + + " \"compatSdkProviderClassName\": \"com.sdk.SdkProvider\"\n" + + "}%n")); + } + + private static void createBundle(Path bundlePath) throws IOException { + createBundle(bundlePath, ResourceTable.getDefaultInstance()); + } + + private static void createBundle(Path bundlePath, ResourceTable resourceTable) + throws IOException { + createBundle(bundlePath, resourceTable, androidManifest("com.app")); + } + + private static void createBundle(Path bundlePath, XmlNode manifest) throws IOException { + createBundle(bundlePath, ResourceTable.getDefaultInstance(), manifest); + } + + private static void createBundle(Path bundlePath, ResourceTable resourceTable, XmlNode manifest) + throws IOException { + SdkBundle sdkBundle = + SdkBundle.builder() + .setModule( + BundleModule.builder() + .setName(BundleModuleName.BASE_MODULE_NAME) + .setBundleType(BundleType.REGULAR) + .setBundletoolVersion(Version.of("1.1.1")) + .setAndroidManifestProto(manifest) + .setResourceTable(resourceTable) + .build()) + .setSdkModulesConfig( + SdkModulesConfig.newBuilder() + .setBundletool(Bundletool.newBuilder().setVersion("1.2.3")) + .setSdkPackageName("com.sdk") + .setSdkVersion( + RuntimeEnabledSdkVersion.newBuilder() + .setMajor(1) + .setMinor(2) + .setPatch(3) + .build()) + .setSdkProviderClassName("com.sdk.SandboxedSdkProviderAdapter") + .setCompatSdkProviderClassName("com.sdk.SdkProvider") + .build()) + .setSdkBundleConfig(SdkBundleConfig.getDefaultInstance()) + .setBundleMetadata(BundleMetadata.builder().build()) + .build(); + + new SdkBundleSerializer().writeToDisk(sdkBundle, bundlePath); + } +} 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 7f9bb8df..52ed0af9 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 @@ -160,12 +160,12 @@ public void getApplicationAppCategory_equalsGame() { xmlNode( xmlElement( "application", - xmlAttribute( + xmlDecimalIntegerAttribute( ANDROID_NAMESPACE_URI, "appCategory", APP_CATEGORY_RESOURCE_ID, - "game")))))); - assertThat(androidManifest.getApplicationAppCategory()).hasValue("game"); + 0)))))); + assertThat(androidManifest.getApplicationAppCategory()).hasValue(0); } @Test diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java index e3842a61..9a5af586 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestDeliveryElementTest.java @@ -16,7 +16,14 @@ package com.android.tools.build.bundletool.model; +import static com.android.tools.build.bundletool.model.AndroidManifest.CONDITIONS_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.CONDITION_USER_COUNTRIES_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.DELIVERY_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.FAST_FOLLOW_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_TIME_ELEMENT_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.ON_DEMAND_ELEMENT_NAME; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withAssetModuleTargeting; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withDeviceGroupsCondition; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withEmptyDeliveryElement; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withFastFollowDelivery; @@ -30,12 +37,18 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkCondition; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withOnDemandDelivery; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withUnsupportedAssetModuleTargeting; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withUnsupportedCondition; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withUserCountriesCondition; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.aapt.Resources.XmlNode; +import com.android.bundle.Targeting.AssetModuleTargeting; +import com.android.bundle.Targeting.DeviceGroupModuleTargeting; +import com.android.bundle.Targeting.UserCountriesTargeting; +import com.android.tools.build.bundletool.model.BundleModule.ModuleType; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement; @@ -58,8 +71,7 @@ public class ManifestDeliveryElementTest { public void emptyDeliveryElement_notWellFormed() { Optional deliveryElement = ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withEmptyDeliveryElement()), - /* isFastFollowAllowed= */ false); + androidManifest("com.test.app", withEmptyDeliveryElement()), ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isFalse(); @@ -73,8 +85,7 @@ public void emptyDeliveryElement_notWellFormed() { public void installTimeDeliveryOnly() { Optional deliveryElement = ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withInstallTimeDelivery()), - /* isFastFollowAllowed= */ false); + androidManifest("com.test.app", withInstallTimeDelivery()), ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isTrue(); @@ -88,8 +99,7 @@ public void installTimeDeliveryOnly() { public void onDemandDeliveryOnly() { Optional deliveryElement = ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withOnDemandDelivery()), - /* isFastFollowAllowed= */ false); + androidManifest("com.test.app", withOnDemandDelivery()), ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isFalse(); @@ -103,8 +113,7 @@ public void onDemandDeliveryOnly() { public void fastFollowDeliveryOnly_fastFollowAllowed() { Optional deliveryElement = ManifestDeliveryElement.fromManifestRootNode( - androidManifest("com.test.app", withFastFollowDelivery()), - /* isFastFollowAllowed= */ true); + androidManifest("com.test.app", withFastFollowDelivery()), ModuleType.ASSET_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isFalse(); @@ -119,7 +128,7 @@ public void onDemandAndInstallTimeDelivery() { Optional deliveryElement = ManifestDeliveryElement.fromManifestRootNode( androidManifest("com.test.app", withInstallTimeDelivery(), withOnDemandDelivery()), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isTrue(); @@ -138,7 +147,7 @@ public void onDemandAndInstallTimeAndFastFollowDelivery_fastFollowAllowed() { withInstallTimeDelivery(), withOnDemandDelivery(), withFastFollowDelivery()), - /* isFastFollowAllowed= */ true); + ModuleType.ASSET_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isTrue(); @@ -153,7 +162,7 @@ public void instantOnDemandDelivery() { Optional deliveryElement = ManifestDeliveryElement.instantFromManifestRootNode( androidManifest("com.test.app", withInstantOnDemandDelivery()), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isFalse(); @@ -168,7 +177,7 @@ public void instantInstallTimeDelivery() { Optional deliveryElement = ManifestDeliveryElement.instantFromManifestRootNode( androidManifest("com.test.app", withInstantInstallTimeDelivery()), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); assertThat(deliveryElement.get().hasInstallTimeElement()).isTrue(); @@ -187,7 +196,7 @@ public void getModuleConditions_returnsAllConditions() { withFeatureCondition("android.hardware.camera.ar"), withMinSdkCondition(24), withMaxSdkCondition(27)), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); @@ -207,7 +216,7 @@ public void getModuleConditions_illegalMinMaxSdk() { Optional deliveryElement = ManifestDeliveryElement.fromManifestRootNode( androidManifest("com.test.app", withMinSdkCondition(27), withMaxSdkCondition(20)), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); Throwable exception = assertThrows( @@ -227,7 +236,7 @@ public void getDeviceFeatureConditions_returnsAllConditions() { withFeatureCondition("android.hardware.camera.ar"), withFeatureCondition("android.software.vr.mode"), withMinSdkVersion(24)), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); @@ -246,7 +255,7 @@ public void moduleConditions_deviceFeatureVersions() { "com.test.app", withFeatureConditionHexVersion("android.software.opengl", 0x30000), withFeatureCondition("android.hardware.vr.headtracking", 1)), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); @@ -262,7 +271,7 @@ public void moduleConditions_unsupportedCondition_throws() throws Exception { Optional manifestDeliveryElement = ManifestDeliveryElement.fromManifestRootNode( androidManifest("com.test.app", withFusingAttribute(false), withUnsupportedCondition()), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(manifestDeliveryElement).isPresent(); @@ -287,7 +296,7 @@ public void moduleConditions_missingNameOfFeature_throws() throws Exception { Optional manifestDeliveryElement = ManifestDeliveryElement.fromManifestRootNode( - createAndroidManifestWithConditions(badCondition), /* isFastFollowAllowed= */ false); + createAndroidManifestWithConditions(badCondition), ModuleType.FEATURE_MODULE); assertThat(manifestDeliveryElement).isPresent(); @@ -311,7 +320,7 @@ public void moduleConditions_missingMinSdkValue_throws() { Optional manifestDeliveryElement = ManifestDeliveryElement.fromManifestRootNode( - createAndroidManifestWithConditions(badCondition), /* isFastFollowAllowed= */ false); + createAndroidManifestWithConditions(badCondition), ModuleType.FEATURE_MODULE); assertThat(manifestDeliveryElement).isPresent(); @@ -329,7 +338,7 @@ public void getModuleConditions_multipleMinSdkCondition_throws() { Optional element = ManifestDeliveryElement.fromManifestRootNode( androidManifest("com.test.app", withMinSdkCondition(24), withMinSdkCondition(28)), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(element).isPresent(); InvalidBundleException exception = @@ -343,9 +352,10 @@ public void getModuleConditions_multipleMinSdkCondition_throws() { public void moduleConditions_typoInElement_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "install-time") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, INSTALL_TIME_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create( DISTRIBUTION_NAMESPACE_URI, "condtions")))); @@ -355,7 +365,7 @@ public void moduleConditions_typoInElement_throws() { InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false)); + nodeWithTypo, ModuleType.FEATURE_MODULE)); assertThat(exception) .hasMessageThat() @@ -371,7 +381,7 @@ public void moduleConditions_deviceGroupsCondition() { ManifestDeliveryElement.fromManifestRootNode( androidManifest( "com.test.app", withDeviceGroupsCondition(ImmutableList.of("group1", "group2"))), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); @@ -388,7 +398,7 @@ public void moduleConditions_multipleDeviceGroupsCondition_throws() { "com.test.app", withDeviceGroupsCondition(ImmutableList.of("group1", "group2")), withDeviceGroupsCondition(ImmutableList.of("group3"))), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(element).isPresent(); @@ -404,7 +414,7 @@ public void moduleConditions_emptyDeviceGroupsCondition_throws() { Optional element = ManifestDeliveryElement.fromManifestRootNode( androidManifest("com.test.app", withDeviceGroupsCondition(ImmutableList.of())), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(element).isPresent(); @@ -426,7 +436,7 @@ public void moduleConditions_wrongElementInsideDeviceGroupsCondition_throws() { Optional element = ManifestDeliveryElement.fromManifestRootNode( createAndroidManifestWithConditions(badDeviceGroupCondition), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(element).isPresent(); @@ -453,7 +463,7 @@ public void moduleConditions_wrongAttributeInDeviceGroupElement_throws() { Optional element = ManifestDeliveryElement.fromManifestRootNode( createAndroidManifestWithConditions(badDeviceGroupCondition), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(element).isPresent(); @@ -472,7 +482,7 @@ public void moduleConditions_wrongDeviceGroupName_throws() { ManifestDeliveryElement.fromManifestRootNode( androidManifest( "com.test.app", withDeviceGroupsCondition(ImmutableList.of("group!!!"))), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(element).isPresent(); @@ -490,7 +500,7 @@ public void moduleConditions_wrongDeviceGroupName_throws() { public void deliveryElement_typoInChildElement_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "instal-time"))); @@ -499,7 +509,7 @@ public void deliveryElement_typoInChildElement_throws() { InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false)); + nodeWithTypo, ModuleType.FEATURE_MODULE)); assertThat(exception) .hasMessageThat() @@ -513,7 +523,7 @@ public void deliveryElement_typoInChildElement_throws() { public void deliveryElement_typoInChildElement_throws_fastFollowEnabled() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "instal-time"))); @@ -522,33 +532,35 @@ public void deliveryElement_typoInChildElement_throws_fastFollowEnabled() { InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ true)); + nodeWithTypo, ModuleType.ASSET_MODULE)); assertThat(exception) .hasMessageThat() .contains( - "Expected element to contain only , " - + ", elements but found: 'instal-time' " - + "with namespace URI: 'http://schemas.android.com/apk/distribution'"); + "Expected element to contain only ," + + " , , elements but found:" + + " 'instal-time' with namespace URI:" + + " 'http://schemas.android.com/apk/distribution'"); } @Test public void fastFollowElement_childElement_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "fast-follow") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, FAST_FOLLOW_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create( - DISTRIBUTION_NAMESPACE_URI, "conditions")))); + DISTRIBUTION_NAMESPACE_URI, CONDITIONS_ELEMENT_NAME)))); InvalidBundleException exception = assertThrows( InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ true)); + nodeWithTypo, ModuleType.ASSET_MODULE)); assertThat(exception) .hasMessageThat() @@ -562,19 +574,20 @@ public void fastFollowElement_childElement_throws() { public void onDemandElement_childElement_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "on-demand") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, ON_DEMAND_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create( - DISTRIBUTION_NAMESPACE_URI, "conditions")))); + DISTRIBUTION_NAMESPACE_URI, CONDITIONS_ELEMENT_NAME)))); InvalidBundleException exception = assertThrows( InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false)); + nodeWithTypo, ModuleType.FEATURE_MODULE)); assertThat(exception) .hasMessageThat() @@ -588,15 +601,15 @@ public void onDemandElement_childElement_throws() { public void onDemandElement_missingNamespace_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") - .addChildElement(XmlProtoElementBuilder.create("on-demand"))); + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) + .addChildElement(XmlProtoElementBuilder.create(ON_DEMAND_ELEMENT_NAME))); InvalidBundleException exception = assertThrows( InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false)); + nodeWithTypo, ModuleType.FEATURE_MODULE)); assertThat(exception) .hasMessageThat() @@ -609,15 +622,15 @@ public void onDemandElement_missingNamespace_throws() { public void installTimeElement_missingNamespace_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") - .addChildElement(XmlProtoElementBuilder.create("install-time"))); + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) + .addChildElement(XmlProtoElementBuilder.create(INSTALL_TIME_ELEMENT_NAME))); InvalidBundleException exception = assertThrows( InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false)); + nodeWithTypo, ModuleType.FEATURE_MODULE)); assertThat(exception) .hasMessageThat() @@ -631,17 +644,18 @@ public void installTimeElement_missingNamespace_throws() { public void conditionsElement_missingNamespace_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "install-time") - .addChildElement(XmlProtoElementBuilder.create("conditions")))); + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, INSTALL_TIME_ELEMENT_NAME) + .addChildElement(XmlProtoElementBuilder.create(CONDITIONS_ELEMENT_NAME)))); InvalidBundleException exception = assertThrows( InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false)); + nodeWithTypo, ModuleType.FEATURE_MODULE)); assertThat(exception) .hasMessageThat() @@ -654,11 +668,13 @@ public void conditionsElement_missingNamespace_throws() { public void minSdkCondition_missingNamespace_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "install-time") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, INSTALL_TIME_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "conditions") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, CONDITIONS_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create( DISTRIBUTION_NAMESPACE_URI, "min-sdk") @@ -671,7 +687,7 @@ public void minSdkCondition_missingNamespace_throws() { InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false) + nodeWithTypo, ModuleType.FEATURE_MODULE) .get() .getModuleConditions()); @@ -684,11 +700,13 @@ public void minSdkCondition_missingNamespace_throws() { public void deviceFeatureCondition_missingNamespace_throws() { XmlNode nodeWithTypo = createAndroidManifestWithDeliveryElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "install-time") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, INSTALL_TIME_ELEMENT_NAME) .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "conditions") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, CONDITIONS_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create( DISTRIBUTION_NAMESPACE_URI, "device-feature") @@ -701,7 +719,7 @@ public void deviceFeatureCondition_missingNamespace_throws() { InvalidBundleException.class, () -> ManifestDeliveryElement.fromManifestRootNode( - nodeWithTypo, /* isFastFollowAllowed= */ false) + nodeWithTypo, ModuleType.FEATURE_MODULE) .get() .getModuleConditions()); @@ -715,12 +733,12 @@ public void deviceFeatureCondition_missingNamespace_throws() { public void userCountriesCondition_parsesOk() { XmlNode manifest = createAndroidManifestWithConditions( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "user-countries") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, CONDITION_USER_COUNTRIES_NAME) .addChildElement(createCountryCodeEntry("pl")) .addChildElement(createCountryCodeEntry("GB")) .build()); Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode(manifest, /* isFastFollowAllowed= */ false); + ManifestDeliveryElement.fromManifestRootNode(manifest, ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); Optional userCountriesCondition = @@ -735,7 +753,7 @@ public void userCountriesCondition_parsesOk() { public void userCountriesCondition_parsesExclusionOk() { XmlNode manifest = createAndroidManifestWithConditions( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "user-countries") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, CONDITION_USER_COUNTRIES_NAME) .addAttribute( XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, "exclude") .setValueAsBoolean(true)) @@ -743,7 +761,7 @@ public void userCountriesCondition_parsesExclusionOk() { .addChildElement(createCountryCodeEntry("SN")) .build()); Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode(manifest, /* isFastFollowAllowed= */ false); + ManifestDeliveryElement.fromManifestRootNode(manifest, ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); Optional userCountriesCondition = @@ -758,7 +776,7 @@ public void userCountriesCondition_parsesExclusionOk() { public void userCountriesCondition_badCountryElementName_throws() { XmlNode manifest = createAndroidManifestWithConditions( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "user-countries") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, CONDITION_USER_COUNTRIES_NAME) .addChildElement( XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "country-typo") .addAttribute( @@ -766,7 +784,7 @@ public void userCountriesCondition_badCountryElementName_throws() { .setValueAsString("DE"))) .build()); Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode(manifest, /* isFastFollowAllowed= */ false); + ManifestDeliveryElement.fromManifestRootNode(manifest, ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); InvalidBundleException exception = @@ -783,7 +801,7 @@ public void userCountriesCondition_badCountryElementName_throws() { public void userCountriesCondition_missingCodeAttribute_throws() { XmlNode manifest = createAndroidManifestWithConditions( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "user-countries") + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, CONDITION_USER_COUNTRIES_NAME) .addChildElement( XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "country") .addAttribute( @@ -791,7 +809,7 @@ public void userCountriesCondition_missingCodeAttribute_throws() { .setValueAsString("DE"))) .build()); Optional deliveryElement = - ManifestDeliveryElement.fromManifestRootNode(manifest, /* isFastFollowAllowed= */ false); + ManifestDeliveryElement.fromManifestRootNode(manifest, ModuleType.FEATURE_MODULE); assertThat(deliveryElement).isPresent(); InvalidBundleException exception = @@ -812,7 +830,7 @@ public void getModuleConditions_multipleUserCountriesConditions_throws() { "com.test.app", withUserCountriesCondition(ImmutableList.of("en", "us")), withUserCountriesCondition(ImmutableList.of("sg"), /* exclude= */ true)), - /* isFastFollowAllowed= */ false); + ModuleType.FEATURE_MODULE); assertThat(element).isPresent(); InvalidBundleException exception = @@ -822,6 +840,43 @@ public void getModuleConditions_multipleUserCountriesConditions_throws() { .contains("Multiple '' conditions are not supported."); } + @Test + public void getAssetModuleConditions() { + AssetModuleTargeting targeting = + AssetModuleTargeting.newBuilder() + .setDeviceGroupTargeting( + DeviceGroupModuleTargeting.newBuilder().addValue("group1").addValue("group2")) + .setUserCountriesTargeting( + UserCountriesTargeting.newBuilder() + .addCountryCodes("US") + .addCountryCodes("GB") + .setExclude(true)) + .build(); + + Optional element = + ManifestDeliveryElement.fromManifestRootNode( + androidManifest("com.test.app", withAssetModuleTargeting(targeting)), + ModuleType.ASSET_MODULE); + assertThat(element).isPresent(); + + assertThat(element.get().getAssetModuleConditions()).isEqualTo(targeting); + } + + @Test + public void getAssetModuleConditions_unrecognizedCondition() { + Optional element = + ManifestDeliveryElement.fromManifestRootNode( + androidManifest("com.test.app", withUnsupportedAssetModuleTargeting()), + ModuleType.ASSET_MODULE); + assertThat(element).isPresent(); + + Throwable exception = + assertThrows(InvalidBundleException.class, () -> element.get().getAssetModuleConditions()); + assertThat(exception) + .hasMessageThat() + .contains("Unrecognized module condition: 'unsupportedCondition'"); + } + private static XmlNode createAndroidManifestWithDeliveryElement( XmlProtoElementBuilder deliveryElement) { return XmlProtoNode.createElementNode( @@ -835,7 +890,7 @@ private static XmlNode createAndroidManifestWithDeliveryElement( private static XmlNode createAndroidManifestWithConditions(XmlProtoElement... conditions) { XmlProtoElementBuilder conditionsBuilder = - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "conditions"); + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, CONDITIONS_ELEMENT_NAME); for (XmlProtoElement condition : conditions) { conditionsBuilder.addChildElement(condition.toBuilder()); } @@ -845,10 +900,11 @@ private static XmlNode createAndroidManifestWithConditions(XmlProtoElement... co .addChildElement( XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "module") .addChildElement( - XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "delivery") + XmlProtoElementBuilder.create( + DISTRIBUTION_NAMESPACE_URI, DELIVERY_ELEMENT_NAME) .addChildElement( XmlProtoElementBuilder.create( - DISTRIBUTION_NAMESPACE_URI, "install-time") + DISTRIBUTION_NAMESPACE_URI, INSTALL_TIME_ELEMENT_NAME) .addChildElement(conditionsBuilder)))) .build()) .getProto(); diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java index c2a111bb..91996c5b 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestEditorTest.java @@ -32,8 +32,6 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.INSTALL_TIME_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.INTENT_FILTER_ELEMENT_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.IS_FEATURE_SPLIT_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_ATTRIBUTE_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.IS_SPLIT_REQUIRED_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.LOCALE_CONFIG_RESOURCE_ID; import static com.android.tools.build.bundletool.model.AndroidManifest.MIN_SDK_VERSION_RESOURCE_ID; @@ -392,15 +390,13 @@ public void setVersionCode() { public void setAppCategory() { AndroidManifest androidManifest = createManifestWithApplicationElement(); - AndroidManifest editedManifest = androidManifest.toEditor().setAppCategory("game").save(); + AndroidManifest editedManifest = + androidManifest.toEditor().setAppCategory(0).save(); // 0 is the category id for "game" assertThat(getApplicationElement(editedManifest).getAttributeList()) .containsExactly( - xmlAttribute( - ANDROID_NAMESPACE_URI, - APP_CATEGORY_ATTRIBUTE_NAME, - APP_CATEGORY_RESOURCE_ID, - "game")); + xmlDecimalIntegerAttribute( + ANDROID_NAMESPACE_URI, APP_CATEGORY_ATTRIBUTE_NAME, APP_CATEGORY_RESOURCE_ID, 0)); } @Test @@ -488,13 +484,6 @@ public void setSplitsRequired() throws Exception { editedManifest, "com.android.vending.splits.required", xmlBooleanAttribute(ANDROID_NAMESPACE_URI, "value", VALUE_RESOURCE_ID, true)); - assertThat(getApplicationElement(editedManifest).getAttributeList()) - .containsExactly( - xmlBooleanAttribute( - ANDROID_NAMESPACE_URI, - IS_SPLIT_REQUIRED_ATTRIBUTE_NAME, - IS_SPLIT_REQUIRED_RESOURCE_ID, - true)); } @Test @@ -512,13 +501,6 @@ public void setSplitsRequired_idempotent() throws Exception { editedManifest, "com.android.vending.splits.required", xmlBooleanAttribute(ANDROID_NAMESPACE_URI, "value", VALUE_RESOURCE_ID, true)); - assertThat(getApplicationElement(editedManifest).getAttributeList()) - .containsExactly( - xmlBooleanAttribute( - ANDROID_NAMESPACE_URI, - IS_SPLIT_REQUIRED_ATTRIBUTE_NAME, - IS_SPLIT_REQUIRED_RESOURCE_ID, - true)); } @Test @@ -536,33 +518,19 @@ public void setSplitsRequired_lastInvocationWins() throws Exception { editedManifest, "com.android.vending.splits.required", xmlBooleanAttribute(ANDROID_NAMESPACE_URI, "value", VALUE_RESOURCE_ID, false)); - assertThat(getApplicationElement(editedManifest).getAttributeList()) - .containsExactly( - xmlBooleanAttribute( - ANDROID_NAMESPACE_URI, - IS_SPLIT_REQUIRED_ATTRIBUTE_NAME, - IS_SPLIT_REQUIRED_RESOURCE_ID, - false)); } @Test public void setSplitTypes() throws Exception { AndroidManifest androidManifest = createManifestWithApplicationElement(); - AndroidManifest editedManifest = - androidManifest - .toEditor() - .setSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) - .save(); + AndroidManifest editedManifest = androidManifest.toEditor().setSplitTypes(SPLIT_NAMES).save(); assertThat(editedManifest.getManifestElement().getAttributes()) .containsExactly( XmlProtoAttributeBuilder.createAndroidAttribute( SPLIT_TYPES_ATTRIBUTE_NAME, SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } @@ -571,20 +539,13 @@ public void setSplitTypes_idempotent() throws Exception { AndroidManifest androidManifest = createManifestWithApplicationElement(); AndroidManifest editedManifest = - androidManifest - .toEditor() - .setSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) - .setSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) - .save(); + androidManifest.toEditor().setSplitTypes(SPLIT_NAMES).setSplitTypes(SPLIT_NAMES).save(); assertThat(editedManifest.getManifestElement().getAttributes()) .containsExactly( XmlProtoAttributeBuilder.createAndroidAttribute( SPLIT_TYPES_ATTRIBUTE_NAME, SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } @@ -593,20 +554,13 @@ public void setSplitTypes_sorted() throws Exception { AndroidManifest androidManifest = createManifestWithApplicationElement(); AndroidManifest editedManifest = - androidManifest - .toEditor() - .setSplitTypes( - ImmutableList.of("language", "config"), /* enableSystemAttribute= */ true) - .save(); + androidManifest.toEditor().setSplitTypes(ImmutableList.of("language", "config")).save(); assertThat(editedManifest.getManifestElement().getAttributes()) .containsExactly( XmlProtoAttributeBuilder.createAndroidAttribute( SPLIT_TYPES_ATTRIBUTE_NAME, SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } @@ -617,8 +571,8 @@ public void setSplitTypes_lastInvocationWins() throws Exception { AndroidManifest editedManifest = androidManifest .toEditor() - .setSplitTypes(ImmutableList.of("base,feature"), /* enableSystemAttribute= */ true) - .setSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) + .setSplitTypes(ImmutableList.of("base,feature")) + .setSplitTypes(SPLIT_NAMES) .save(); assertThat(editedManifest.getManifestElement().getAttributes()) @@ -626,9 +580,6 @@ public void setSplitTypes_lastInvocationWins() throws Exception { XmlProtoAttributeBuilder.createAndroidAttribute( SPLIT_TYPES_ATTRIBUTE_NAME, SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } @@ -637,20 +588,13 @@ public void setRequiredSplitTypes() throws Exception { AndroidManifest androidManifest = createManifestWithApplicationElement(); AndroidManifest editedManifest = - androidManifest - .toEditor() - .setRequiredSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) - .save(); + androidManifest.toEditor().setRequiredSplitTypes(SPLIT_NAMES).save(); assertThat(editedManifest.getManifestElement().getAttributes()) .containsExactly( XmlProtoAttributeBuilder.createAndroidAttribute( REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create( - DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } @@ -661,8 +605,8 @@ public void setRequiredSplitTypes_idempotent() throws Exception { AndroidManifest editedManifest = androidManifest .toEditor() - .setRequiredSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) - .setRequiredSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) + .setRequiredSplitTypes(SPLIT_NAMES) + .setRequiredSplitTypes(SPLIT_NAMES) .save(); assertThat(editedManifest.getManifestElement().getAttributes()) @@ -670,10 +614,6 @@ public void setRequiredSplitTypes_idempotent() throws Exception { XmlProtoAttributeBuilder.createAndroidAttribute( REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create( - DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } @@ -684,8 +624,7 @@ public void setRequiredSplitTypes_sorted() throws Exception { AndroidManifest editedManifest = androidManifest .toEditor() - .setRequiredSplitTypes( - ImmutableList.of("language", "config"), /* enableSystemAttribute= */ true) + .setRequiredSplitTypes(ImmutableList.of("language", "config")) .save(); assertThat(editedManifest.getManifestElement().getAttributes()) @@ -693,10 +632,6 @@ public void setRequiredSplitTypes_sorted() throws Exception { XmlProtoAttributeBuilder.createAndroidAttribute( REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create( - DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } @@ -707,9 +642,8 @@ public void setRequiredSplitTypes_lastInvocationWins() throws Exception { AndroidManifest editedManifest = androidManifest .toEditor() - .setRequiredSplitTypes( - ImmutableList.of("base,feature"), /* enableSystemAttribute= */ true) - .setRequiredSplitTypes(SPLIT_NAMES, /* enableSystemAttribute= */ true) + .setRequiredSplitTypes(ImmutableList.of("base,feature")) + .setRequiredSplitTypes(SPLIT_NAMES) .save(); assertThat(editedManifest.getManifestElement().getAttributes()) @@ -717,10 +651,6 @@ public void setRequiredSplitTypes_lastInvocationWins() throws Exception { XmlProtoAttributeBuilder.createAndroidAttribute( REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID) .setValueAsString("config,language") - .build(), - XmlProtoAttributeBuilder.create( - DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) - .setValueAsString("config,language") .build()); } diff --git a/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java b/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java index f52da33a..02169eb7 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/ManifestMutatorTest.java @@ -17,7 +17,6 @@ package com.android.tools.build.bundletool.model; import static com.android.tools.build.bundletool.model.ManifestMutator.withExtractNativeLibs; -import static com.android.tools.build.bundletool.model.ManifestMutator.withSplitsRequired; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.google.common.truth.Truth.assertThat; @@ -42,17 +41,4 @@ public void setExtractNativeLibsValue() throws Exception { editedManifest = editedManifest.applyMutators(ImmutableList.of(withExtractNativeLibs(true))); assertThat(editedManifest.getExtractNativeLibsValue()).hasValue(true); } - - @Test - public void setSplitsRequiredValue() throws Exception { - AndroidManifest manifest = AndroidManifest.create(androidManifest("com.test.app")); - assertThat(manifest.getSplitsRequiredValue()).isEmpty(); - - AndroidManifest editedManifest = - manifest.applyMutators(ImmutableList.of(withSplitsRequired(false))); - assertThat(editedManifest.getSplitsRequiredValue()).hasValue(false); - - editedManifest = editedManifest.applyMutators(ImmutableList.of(withSplitsRequired(true))); - assertThat(editedManifest.getSplitsRequiredValue()).hasValue(true); - } } diff --git a/src/test/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjectorTest.java b/src/test/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjectorTest.java index 698ffccf..4594cf0a 100644 --- a/src/test/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjectorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/model/RequiredSplitTypesInjectorTest.java @@ -16,9 +16,9 @@ package com.android.tools.build.bundletool.model; -import static com.android.tools.build.bundletool.model.AndroidManifest.DISTRIBUTION_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME; -import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_ATTRIBUTE_NAME; +import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_RESOURCE_ID; +import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_RESOURCE_ID; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkAbiTargeting; import static com.android.tools.build.bundletool.testing.TargetingUtils.apkCountrySetTargeting; @@ -68,8 +68,7 @@ public void writeSplitTypeValidationInManifest_setsRequiredSplitTypesForModule() ImmutableList.of(baseSplit.getModuleName(), featureSplit.getModuleName()); ImmutableList allSplits = ImmutableList.of(baseSplit, featureSplit); ImmutableList newSplits = - RequiredSplitTypesInjector.injectSplitTypeValidation( - allSplits, requiredModules, /* enableSystemAttribute= */ true); + RequiredSplitTypesInjector.injectSplitTypeValidation(allSplits, requiredModules); baseSplit = newSplits.get(0); assertThat(getProvidedSplitTypes(baseSplit)).isEmpty(); @@ -114,8 +113,7 @@ public void writeSplitTypeValidationInManifest_setsRequiredSplitTypesForTargetin ImmutableList requiredModules = ImmutableList.of(baseSplit.getModuleName()); ImmutableList allSplits = ImmutableList.of(baseSplit, otherSplit); ImmutableList newSplits = - RequiredSplitTypesInjector.injectSplitTypeValidation( - allSplits, requiredModules, /* enableSystemAttribute= */ true); + RequiredSplitTypesInjector.injectSplitTypeValidation(allSplits, requiredModules); baseSplit = newSplits.get(0); assertThat(getProvidedSplitTypes(baseSplit)).isEmpty(); @@ -132,10 +130,10 @@ private static ImmutableList getRequiredSplitTypes(ModuleSplit moduleSpl moduleSplit .getAndroidManifest() .getManifestElement() - .getAttribute(DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .getAndroidAttribute(REQUIRED_SPLIT_TYPES_RESOURCE_ID) .orElse( - XmlProtoAttribute.create( - DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME)) + XmlProtoAttribute.createAndroidAttribute( + REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID)) .getValueAsString(); if (value.isEmpty()) { return ImmutableList.of(); @@ -148,7 +146,7 @@ private static ImmutableList getProvidedSplitTypes(ModuleSplit moduleSpl moduleSplit .getAndroidManifest() .getManifestElement() - .getAttribute(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .getAndroidAttribute(SPLIT_TYPES_RESOURCE_ID) .get() .getValueAsString(); if (value.isEmpty()) { 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 89e8aecc..aa6a1b51 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 @@ -16,10 +16,8 @@ package com.android.tools.build.bundletool.splitters; -import static com.android.tools.build.bundletool.model.AndroidManifest.DISTRIBUTION_NAMESPACE_URI; import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.REQUIRED_SPLIT_TYPES_RESOURCE_ID; -import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SPLIT_TYPES_RESOURCE_ID; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_L_API_VERSION; import static com.android.tools.build.bundletool.model.utils.Versions.ANDROID_M_API_VERSION; @@ -217,102 +215,6 @@ public void simpleMultipleModules_withRequiredSplitTypes() throws Exception { assertConsistentRequiredSplitTypes(moduleSplits); } - @Test - public void simpleMultipleModules_withRequiredSplitTypes_experimentalTPlusVariant() - throws Exception { - TestComponent.useTestModule(this, TestModule.builder().build()); - ImmutableList bundleModule = - ImmutableList.of( - new BundleModuleBuilder("base") - .addFile("assets/leftover.txt") - .setManifest(androidManifest("com.test.app")) - .build(), - new BundleModuleBuilder("test") - .addFile("assets/test.txt") - .setManifest(androidManifest("com.test.app")) - .build()); - - ImmutableList moduleSplits = - splitApksGenerator.generateSplits( - bundleModule, - ApkGenerationConfiguration.builder().setEnableRequiredSplitTypes(true).build()); - - assertThat(moduleSplits).hasSize(4); - ImmutableMap moduleSplitMap = - Maps.uniqueIndex( - moduleSplits, - split -> - String.format( - "%s:%s", - split.getModuleName().getName(), - split - .getVariantTargeting() - .getSdkVersionTargeting() - .getValue(0) - .getMin() - .getValue())); - - assertThat(moduleSplitMap.keySet()).containsExactly("base:21", "test:21", "base:33", "test:33"); - - ModuleSplit baseModule = moduleSplitMap.get("base:21"); - assertThat(baseModule).isNotNull(); - assertThat(getRequiredSplitTypes(baseModule)).containsExactly("test__module"); - assertThat(getProvidedSplitTypes(baseModule)).isEmpty(); - - ModuleSplit testModule = moduleSplitMap.get("test:21"); - assertThat(testModule).isNotNull(); - assertThat(getRequiredSplitTypes(testModule)).isEmpty(); - assertThat(getProvidedSplitTypes(testModule)).containsExactly("test__module"); - - // TODO(b/199376532): Remove once system required split type attributes are enabled. - ModuleSplit baseModuleTPlus = moduleSplitMap.get("base:33"); - assertThat(baseModuleTPlus).isNotNull(); - assertThat(getRequiredSplitTypes(baseModuleTPlus)).containsExactly("test__module"); - assertThat(getProvidedSplitTypes(baseModuleTPlus)).isEmpty(); - ModuleSplit testModuleTPlus = moduleSplitMap.get("test:33"); - assertThat(testModuleTPlus).isNotNull(); - assertThat(getRequiredSplitTypes(testModuleTPlus)).isEmpty(); - assertThat(getProvidedSplitTypes(testModuleTPlus)).containsExactly("test__module"); - - assertConsistentRequiredSplitTypes(moduleSplits); - } - - @Test - public void simpleMultipleModules_withoutRequiredSplitTypes() throws Exception { - TestComponent.useTestModule(this, TestModule.builder().build()); - ImmutableList bundleModule = - ImmutableList.of( - new BundleModuleBuilder("base") - .addFile("assets/leftover.txt") - .setManifest(androidManifest("com.test.app")) - .build(), - new BundleModuleBuilder("test") - .addFile("assets/test.txt") - .setManifest(androidManifest("com.test.app")) - .build()); - - ImmutableList moduleSplits = - splitApksGenerator.generateSplits( - bundleModule, - ApkGenerationConfiguration.builder().setEnableRequiredSplitTypes(false).build()); - - assertThat(moduleSplits).hasSize(2); - for (ModuleSplit moduleSplit : moduleSplits) { - assertThat( - moduleSplit - .getAndroidManifest() - .getManifestElement() - .getAndroidAttribute(SPLIT_TYPES_RESOURCE_ID)) - .isEmpty(); - assertThat( - moduleSplit - .getAndroidManifest() - .getManifestElement() - .getAndroidAttribute(REQUIRED_SPLIT_TYPES_RESOURCE_ID)) - .isEmpty(); - } - } - @Test public void multipleModules_withOnlyBaseModuleWithNativeLibraries() throws Exception { ImmutableList bundleModule = @@ -799,132 +701,6 @@ public void appBundleWithSdkDependencyModuleAndDensityTargeting_noDensitySplitsF TestComponent.useTestModule( this, TestModule.builder().withAppBundle(appBundleWithRuntimeEnabledSdkDeps).build()); - ImmutableList moduleSplits = - splitApksGenerator.generateSplits( - appBundleWithRuntimeEnabledSdkDeps.getModules().values().asList(), - ApkGenerationConfiguration.builder() - .setOptimizationDimensions(ImmutableSet.of(OptimizationDimension.SCREEN_DENSITY)) - .setEnableRequiredSplitTypes(false) - .build()); - - assertThat( - moduleSplits.stream().map(ModuleSplit::getVariantTargeting).collect(toImmutableSet())) - .containsExactly( - lPlusVariantTargeting(), sdkRuntimeVariantTargeting(ANDROID_U_API_VERSION)); - ImmutableMap> moduleSplitMap = - moduleSplits.stream() - .collect(toImmutableListMultimap(ModuleSplit::getVariantTargeting, Function.identity())) - .asMap(); - assertThat( - moduleSplitMap.get(lPlusVariantTargeting()).stream() - .map(ModuleSplit::getModuleName) - .map(BundleModuleName::getName) - .distinct()) - .containsExactly("base", "comtestsdk"); - ImmutableSet sdkModuleSplits = - moduleSplitMap.get(lPlusVariantTargeting()).stream() - .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("comtestsdk")) - .collect(toImmutableSet()); - // Only main split for SDK dependency module - no config splits. - assertThat(sdkModuleSplits).hasSize(1); - assertThat( - Iterables.getOnlyElement(sdkModuleSplits).getApkTargeting().hasScreenDensityTargeting()) - .isFalse(); - ImmutableSet nonSdkRuntimeVariantBaseModuleSplits = - moduleSplitMap.get(lPlusVariantTargeting()).stream() - .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("base")) - .collect(toImmutableSet()); - // 1 main split + 7 config splits: 1 per each screen density. - assertThat(nonSdkRuntimeVariantBaseModuleSplits).hasSize(8); - assertThat( - moduleSplitMap.get(sdkRuntimeVariantTargeting(ANDROID_U_API_VERSION)).stream() - .map(ModuleSplit::getModuleName) - .map(BundleModuleName::getName) - .distinct()) - .containsExactly("base"); - ImmutableSet sdkRuntimeVariantSplits = - moduleSplitMap.get(sdkRuntimeVariantTargeting(ANDROID_U_API_VERSION)).stream() - .filter(moduleSplit -> moduleSplit.getModuleName().getName().equals("base")) - .collect(toImmutableSet()); - // 1 main split + 7 config splits: 1 per each screen density. - assertThat(sdkRuntimeVariantSplits).hasSize(8); - - for (ModuleSplit moduleSplit : moduleSplits) { - assertThat( - moduleSplit - .getAndroidManifest() - .getManifestElement() - .getAndroidAttribute(SPLIT_TYPES_RESOURCE_ID)) - .isEmpty(); - assertThat( - moduleSplit - .getAndroidManifest() - .getManifestElement() - .getAndroidAttribute(REQUIRED_SPLIT_TYPES_RESOURCE_ID)) - .isEmpty(); - } - } - - @Test - public void - appBundleWithSdkDependencyModuleAndDensityTargeting_noDensitySplitsForSdkModule_requiredSplitTypesSet() { - ResourceTable appResourceTable = - resourceTable( - pkg( - USER_PACKAGE_OFFSET, - "com.test.app", - type( - 0x01, - "drawable", - entry( - 0x01, - "title_image", - fileReference("res/drawable-hdpi/title_image.jpg", HDPI), - fileReference( - "res/drawable/title_image.jpg", Configuration.getDefaultInstance()))))); - ResourceTable sdkResourceTable = - resourceTable( - pkg( - USER_PACKAGE_OFFSET + 1, - "com.test.sdk", - type( - 0x01, - "drawable", - entry( - 0x01, - "title_image", - fileReference("res/drawable-hdpi/title_image.jpg", HDPI), - fileReference( - "res/drawable/title_image.jpg", Configuration.getDefaultInstance()))))); - AppBundle appBundleWithRuntimeEnabledSdkDeps = - new AppBundleBuilder() - .addModule( - new BundleModuleBuilder("base") - .setManifest(androidManifest("com.test.app")) - .setResourceTable(appResourceTable) - .setRuntimeEnabledSdkConfig( - RuntimeEnabledSdkConfig.newBuilder() - .addRuntimeEnabledSdk( - RuntimeEnabledSdk.newBuilder() - .setPackageName("com.test.sdk") - .setVersionMajor(1) - .setCertificateDigest("AA:BB:CC") - .setResourcesPackageId(2)) - .build()) - .setResourcesPackageId(2) - .build()) - .addModule( - new BundleModuleBuilder("comtestsdk") - .setModuleType(ModuleType.SDK_DEPENDENCY_MODULE) - .setSdkModulesConfig(SdkModulesConfig.getDefaultInstance()) - .setResourcesPackageId(2) - .setManifest(androidManifest("com.test.sdk")) - .setResourceTable(sdkResourceTable) - .build()) - .build(); - TestComponent.useTestModule( - this, TestModule.builder().withAppBundle(appBundleWithRuntimeEnabledSdkDeps).build()); - ImmutableList moduleSplits = splitApksGenerator.generateSplits( appBundleWithRuntimeEnabledSdkDeps.getModules().values().asList(), @@ -1338,10 +1114,10 @@ private static ImmutableList getRequiredSplitTypes(ModuleSplit moduleSpl moduleSplit .getAndroidManifest() .getManifestElement() - .getAttribute(DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME) + .getAndroidAttribute(REQUIRED_SPLIT_TYPES_RESOURCE_ID) .orElse( - XmlProtoAttribute.create( - DISTRIBUTION_NAMESPACE_URI, REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME)) + XmlProtoAttribute.createAndroidAttribute( + REQUIRED_SPLIT_TYPES_ATTRIBUTE_NAME, REQUIRED_SPLIT_TYPES_RESOURCE_ID)) .getValueAsString(); if (value.isEmpty()) { return ImmutableList.of(); @@ -1354,7 +1130,7 @@ private static ImmutableList getProvidedSplitTypes(ModuleSplit moduleSpl moduleSplit .getAndroidManifest() .getManifestElement() - .getAttribute(DISTRIBUTION_NAMESPACE_URI, SPLIT_TYPES_ATTRIBUTE_NAME) + .getAndroidAttribute(SPLIT_TYPES_RESOURCE_ID) .get() .getValueAsString(); if (value.isEmpty()) { 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 43116de5..f881ea0d 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 @@ -102,6 +102,7 @@ import com.android.aapt.Resources.XmlNamespace; import com.android.aapt.Resources.XmlNode; import com.android.bundle.Commands.DeliveryType; +import com.android.bundle.Targeting.AssetModuleTargeting; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElementBuilder; @@ -315,6 +316,16 @@ public static XmlNode androidManifest(String packageName, ManifestMutator... man return xmlProtoNode.build().getProto(); } + public static XmlNode androidManifestForSdkBundle( + String packageName, ManifestMutator... manifestMutators) { + return androidManifest( + packageName, + ObjectArrays.concat( + new ManifestMutator[] {withTargetSdkVersion(34)}, + manifestMutators, + ManifestMutator.class)); + } + public static XmlNode androidManifestForFeature( String packageName, ManifestMutator... manifestMutators) { return androidManifest( @@ -537,6 +548,32 @@ public static ManifestMutator withDelivery(DeliveryType deliveryType) { } } + public static ManifestMutator withAssetModuleTargeting(AssetModuleTargeting targeting) { + return manifestElement -> + manifestElement + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "module") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "delivery") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "conditions") + .addChildElement( + createDeviceGroupElement( + ImmutableList.copyOf(targeting.getDeviceGroupTargeting().getValueList()))) + .addChildElement( + createUserCountriesCondition( + ImmutableList.copyOf( + targeting.getUserCountriesTargeting().getCountryCodesList()), + Optional.of(targeting.getUserCountriesTargeting().getExclude()))); + } + + public static ManifestMutator withUnsupportedAssetModuleTargeting() { + return manifestElement -> + manifestElement + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "module") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "delivery") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "conditions") + .addChildElement( + XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "unsupportedCondition")); + } + /** Adds the instant attribute under the dist:module tag. */ public static ManifestMutator withInstant(boolean value) { return manifestElement -> @@ -917,6 +954,19 @@ public static ManifestMutator withMaxSdkCondition(int maxSdkVersion) { } public static ManifestMutator withDeviceGroupsCondition(ImmutableList deviceGroups) { + XmlProtoElementBuilder deviceGroupsElement = createDeviceGroupElement(deviceGroups); + + return manifestElement -> + manifestElement + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "module") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "delivery") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "conditions") + .addChildElement(deviceGroupsElement); + } + + private static XmlProtoElementBuilder createDeviceGroupElement( + ImmutableList deviceGroups) { XmlProtoElementBuilder deviceGroupsElement = XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, CONDITION_DEVICE_GROUPS_NAME); @@ -927,14 +977,7 @@ public static ManifestMutator withDeviceGroupsCondition(ImmutableList de XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, NAME_ATTRIBUTE_NAME) .setValueAsString(deviceGroup))); } - - return manifestElement -> - manifestElement - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "module") - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "delivery") - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time") - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "conditions") - .addChildElement(deviceGroupsElement); + return deviceGroupsElement; } /** @@ -971,6 +1014,19 @@ public static ManifestMutator withRequiredByPrivacySandboxElement( private static ManifestMutator withUserCountriesConditionInternal( ImmutableList codes, Optional exclude) { + XmlProtoElementBuilder userCountries = createUserCountriesCondition(codes, exclude); + + return manifestElement -> + manifestElement + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "module") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "delivery") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time") + .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "conditions") + .addChildElement(userCountries); + } + + private static XmlProtoElementBuilder createUserCountriesCondition( + ImmutableList codes, Optional exclude) { XmlProtoElementBuilder userCountries = XmlProtoElementBuilder.create(DISTRIBUTION_NAMESPACE_URI, "user-countries"); @@ -987,14 +1043,7 @@ private static ManifestMutator withUserCountriesConditionInternal( XmlProtoAttributeBuilder.create(DISTRIBUTION_NAMESPACE_URI, "code") .setValueAsString(countryCode))); } - - return manifestElement -> - manifestElement - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "module") - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "delivery") - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "install-time") - .getOrCreateChildElement(DISTRIBUTION_NAMESPACE_URI, "conditions") - .addChildElement(userCountries); + return userCountries; } public static ManifestMutator withUnsupportedCondition() { diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java index f5fc459c..624a4254 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestModule.java @@ -136,7 +136,6 @@ public static class Builder { @Nullable private PrintStream printStream; @Nullable private Boolean localTestingEnabled; @Nullable private SourceStamp sourceStamp; - private Boolean enableRequiredSplitTypes = true; private BundleMetadata bundleMetadata = DEFAULT_BUNDLE_METADATA; Optional featureModulesCustomConfig = Optional.empty(); @@ -266,11 +265,6 @@ public Builder withSdkBundle(SdkBundle sdkBundle) { return this; } - public Builder withEnableRequiredSplitTypes(boolean enableRequiredSplitTypes) { - this.enableRequiredSplitTypes = enableRequiredSplitTypes; - return this; - } - @CanIgnoreReturnValue public Builder withFeatureModulesCustomConfig( Optional featureModulesCustomConfig) { diff --git a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java index f6222dae..9706c142 100644 --- a/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java +++ b/src/test/java/com/android/tools/build/bundletool/testing/TestUtils.java @@ -22,6 +22,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallLocation; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSdkVersion; import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.DEFAULT_SDK_MODULES_CONFIG; import static com.android.tools.build.bundletool.testing.SdkBundleBuilder.PACKAGE_NAME; import static com.google.common.collect.ImmutableList.toImmutableList; @@ -262,7 +263,7 @@ public static ZipBuilder createZipBuilderForModulesWithInvalidManifest() { } public static XmlNode createSdkAndroidManifest() { - return androidManifest(PACKAGE_NAME, withMinSdkVersion(32)); + return androidManifest(PACKAGE_NAME, withMinSdkVersion(32), withTargetSdkVersion(34)); } public static XmlNode createInvalidSdkAndroidManifest() { diff --git a/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java index 3debc257..23d17c9e 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/AndroidManifestValidatorTest.java @@ -32,6 +32,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withOnDemandAttribute; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withOnDemandDelivery; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSharedUserId; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSupportsGlTexture; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSandboxVersion; @@ -42,6 +43,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.android.bundle.Config.BundleConfig; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdk; +import com.android.bundle.RuntimeEnabledSdkConfigProto.RuntimeEnabledSdkConfig; import com.android.tools.build.bundletool.model.BundleModule; import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttributeBuilder; @@ -1103,4 +1106,30 @@ public void usesSdkLibraryTagPresent_throws() { + " should depend on a runtime-enabled SDK by specifying its details in the" + " runtime_enabled_sdk_config.pb, not in its manifest."); } + + @Test + public void sharedUserIdAndRuntimeEnabledSdkBothPresent_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setRuntimeEnabledSdkConfig( + RuntimeEnabledSdkConfig.newBuilder() + .addRuntimeEnabledSdk( + RuntimeEnabledSdk.newBuilder() + .setPackageName("com.test.sdk") + .setVersionMajor(1) + .setVersionMinor(2) + .setCertificateDigest("cert-digest") + .setResourcesPackageId(2)) + .build()) + .setManifest(androidManifest(PKG_NAME, withSharedUserId("com.test.shared"))) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> new AndroidManifestValidator().validateAllModules(ImmutableList.of(module))); + assertThat(exception) + .hasMessageThat() + .contains("'sharedUserId' cannot be used with runtime-enabled SDKs."); + } } diff --git a/src/test/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkManifestCompatibilityValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkManifestCompatibilityValidatorTest.java new file mode 100644 index 00000000..7493e928 --- /dev/null +++ b/src/test/java/com/android/tools/build/bundletool/validation/RuntimeEnabledSdkManifestCompatibilityValidatorTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.tools.build.bundletool.validation; + +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMinSdkVersion; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSdkVersion; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.android.tools.build.bundletool.model.AppBundle; +import com.android.tools.build.bundletool.model.BundleModule; +import com.android.tools.build.bundletool.model.exceptions.InvalidBundleException; +import com.android.tools.build.bundletool.testing.AppBundleBuilder; +import com.android.tools.build.bundletool.testing.BundleModuleBuilder; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RuntimeEnabledSdkManifestCompatibilityValidatorTest { + + @Test + public void validateBundleWithSdkModules_appMinSdkLowerThanSdkMinSdk_throws() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module.setManifest( + androidManifest( + "com.app", withMinSdkVersion(31), withTargetSdkVersion(35)))) + .build(); + BundleModule sdkModule = + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.sdk", withMinSdkVersion(32), withTargetSdkVersion(35))) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new RuntimeEnabledSdkManifestCompatibilityValidator() + .validateBundleWithSdkModules(appBundle, ImmutableMap.of("sdk1", sdkModule))); + assertThat(exception) + .hasMessageThat() + .contains( + "Runtime-enabled SDKs must have a minSdkVersion lower than the app, but found SDK" + + " 'sdk1' with minSdkVersion (32) higher than the app's minSdkVersion (31)."); + } + + @Test + public void validateBundleWithSdkModules_appMinSdkLowerThanAtLeastOneSdkMinSdk_throws() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module.setManifest( + androidManifest( + "com.app", withMinSdkVersion(31), withTargetSdkVersion(35)))) + .build(); + BundleModule sdkModule1 = + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.sdk", withMinSdkVersion(30), withTargetSdkVersion(35))) + .build(); + BundleModule sdkModule2 = + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.sdk", withMinSdkVersion(32), withTargetSdkVersion(35))) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new RuntimeEnabledSdkManifestCompatibilityValidator() + .validateBundleWithSdkModules( + appBundle, ImmutableMap.of("sdk1", sdkModule1, "sdk2", sdkModule2))); + assertThat(exception) + .hasMessageThat() + .contains( + "Runtime-enabled SDKs must have a minSdkVersion lower than the app, but found SDK" + + " 'sdk2' with minSdkVersion (32) higher than the app's minSdkVersion (31)."); + } + + @Test + public void validateBundleWithSdkModules_targetSdkLowerThanMinSdkOfAnotherSdk_throws() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module.setManifest( + androidManifest( + "com.app", withMinSdkVersion(32), withTargetSdkVersion(35)))) + .build(); + BundleModule sdkModule1 = + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.sdk", withMinSdkVersion(30), withTargetSdkVersion(31))) + .build(); + BundleModule sdkModule2 = + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.sdk", withMinSdkVersion(32), withTargetSdkVersion(35))) + .build(); + + InvalidBundleException exception = + assertThrows( + InvalidBundleException.class, + () -> + new RuntimeEnabledSdkManifestCompatibilityValidator() + .validateBundleWithSdkModules( + appBundle, ImmutableMap.of("sdk1", sdkModule1, "sdk2", sdkModule2))); + assertThat(exception) + .hasMessageThat() + .contains( + "Runtime-enabled SDKs must have a minSdkVersion lower or equal to the targetSdkVersion" + + " of another SDK, but found SDK 'sdk2' with minSdkVersion (32) higher than the" + + " targetSdkVersion (31) of SDK 'sdk1'."); + } + + @Test + public void validateBundleWithSdkModules_ok() { + AppBundle appBundle = + new AppBundleBuilder() + .addModule( + "base", + module -> + module.setManifest( + androidManifest( + "com.app", withMinSdkVersion(31), withTargetSdkVersion(35)))) + .build(); + BundleModule sdkModule1 = + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.sdk", withMinSdkVersion(30), withTargetSdkVersion(35))) + .build(); + BundleModule sdkModule2 = + new BundleModuleBuilder("base") + .setManifest( + androidManifest("com.test.sdk", withMinSdkVersion(31), withTargetSdkVersion(35))) + .build(); + + // No exception thrown. + new RuntimeEnabledSdkManifestCompatibilityValidator() + .validateBundleWithSdkModules( + appBundle, ImmutableMap.of("sdk1", sdkModule1, "sdk2", sdkModule2)); + } +} diff --git a/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java b/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java index ec356ae9..8e0140fd 100644 --- a/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java +++ b/src/test/java/com/android/tools/build/bundletool/validation/SdkAndroidManifestValidatorTest.java @@ -25,6 +25,7 @@ import static com.android.tools.build.bundletool.model.AndroidManifest.SDK_PATCH_VERSION_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.model.AndroidManifest.SHARED_USER_ID_ATTRIBUTE_NAME; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifest; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.androidManifestForSdkBundle; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withInstallLocation; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withMainActivity; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withPermission; @@ -35,6 +36,7 @@ import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSharedUserId; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitId; import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withSplitNameService; +import static com.android.tools.build.bundletool.testing.ManifestProtoUtils.withTargetSdkVersion; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -56,7 +58,7 @@ public void manifest_withSdkLibraryElement_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) .setManifest( - androidManifest( + androidManifestForSdkBundle( PKG_NAME, withSdkLibraryElement(PKG_NAME, /* versionMajor= */ 13499))) .build(); @@ -77,7 +79,8 @@ public void manifest_withSdkPatchVersionProperty_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) .setManifest( - androidManifest(PKG_NAME, withSdkPatchVersionMetadata(/* patchVersion= */ 14))) + androidManifestForSdkBundle( + PKG_NAME, withSdkPatchVersionMetadata(/* patchVersion= */ 14))) .build(); Throwable exception = @@ -98,7 +101,7 @@ public void manifest_withSdkPatchVersionProperty_throws() { public void manifest_withPreferExternal_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) - .setManifest(androidManifest(PKG_NAME, withInstallLocation(2))) + .setManifest(androidManifestForSdkBundle(PKG_NAME, withInstallLocation(2))) .build(); Throwable exception = @@ -117,7 +120,7 @@ public void manifest_withPreferExternal_throws() { public void manifest_withPermission_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) - .setManifest(androidManifest(PKG_NAME, withPermission())) + .setManifest(androidManifestForSdkBundle(PKG_NAME, withPermission())) .build(); Throwable exception = @@ -136,7 +139,7 @@ public void manifest_withPermission_throws() { public void manifest_withPermissionGroup_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) - .setManifest(androidManifest(PKG_NAME, withPermissionGroup())) + .setManifest(androidManifestForSdkBundle(PKG_NAME, withPermissionGroup())) .build(); Throwable exception = @@ -155,7 +158,7 @@ public void manifest_withPermissionGroup_throws() { public void manifest_withPermissionTree_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) - .setManifest(androidManifest(PKG_NAME, withPermissionTree())) + .setManifest(androidManifestForSdkBundle(PKG_NAME, withPermissionTree())) .build(); Throwable exception = @@ -174,7 +177,7 @@ public void manifest_withPermissionTree_throws() { public void manifest_withSharedUserId_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) - .setManifest(androidManifest(PKG_NAME, withSharedUserId("sharedUserId"))) + .setManifest(androidManifestForSdkBundle(PKG_NAME, withSharedUserId("sharedUserId"))) .build(); Throwable exception = @@ -193,7 +196,7 @@ public void manifest_withSharedUserId_throws() { public void manifest_withActivityComponent_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) - .setManifest(androidManifest(PKG_NAME, withMainActivity("myFunActivity"))) + .setManifest(androidManifestForSdkBundle(PKG_NAME, withMainActivity("myFunActivity"))) .build(); Throwable exception = @@ -212,9 +215,8 @@ public void manifest_withServiceComponent_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) .setManifest( - androidManifest( - PKG_NAME, - withSplitNameService("serviceName", "splitName"))) + androidManifestForSdkBundle( + PKG_NAME, withSplitNameService("serviceName", "splitName"))) .build(); Throwable exception = @@ -232,7 +234,7 @@ public void manifest_withServiceComponent_throws() { public void manifest_withSplitId_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME) - .setManifest(androidManifest(PKG_NAME, withSplitId(BASE_MODULE_NAME))) + .setManifest(androidManifestForSdkBundle(PKG_NAME, withSplitId(BASE_MODULE_NAME))) .build(); Throwable exception = @@ -245,10 +247,42 @@ public void manifest_withSplitId_throws() { } @Test - public void manifest_valid_ok() { + public void manifest_withoutTargetSdkVersion_throws() { BundleModule module = new BundleModuleBuilder(BASE_MODULE_NAME).setManifest(androidManifest(PKG_NAME)).build(); + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains("The 'targetSdkVersion' of an SDK bundle should be 34 or higher."); + } + + @Test + public void manifest_withLowTargetSdkVersion_throws() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest(androidManifest(PKG_NAME, withTargetSdkVersion(33))) + .build(); + + Throwable exception = + assertThrows( + InvalidBundleException.class, + () -> new SdkAndroidManifestValidator().validateModule(module)); + assertThat(exception) + .hasMessageThat() + .contains("The 'targetSdkVersion' of an SDK bundle should be 34 or higher."); + } + + @Test + public void manifest_valid_ok() { + BundleModule module = + new BundleModuleBuilder(BASE_MODULE_NAME) + .setManifest(androidManifestForSdkBundle(PKG_NAME)) + .build(); + new SdkAndroidManifestValidator().validateModule(module); } }