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) {
*
* - {@code } element inside the {@code
* } element (read by the PlayCore library).
- *
- {@code } attribute (read by the Android
- * Platform since Q).
*
*/
@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 extends ZipEntry> 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);
}
}