diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index b2f1f62647e68..064d864114633 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -84,6 +84,7 @@ cc_library_host_static { "compile/PseudolocaleGenerator.cpp", "compile/Pseudolocalizer.cpp", "compile/XmlIdCollector.cpp", + "configuration/ConfigurationParser.cpp", "filter/ConfigFilter.cpp", "flatten/Archive.cpp", "flatten/TableFlattener.cpp", @@ -160,7 +161,10 @@ cc_library_host_shared { cc_test_host { name: "aapt2_tests", srcs: ["**/*_test.cpp"], - static_libs: ["libaapt2", "libgmock"], + static_libs: [ + "libaapt2", + "libgmock", + ], defaults: ["aapt_defaults"], } diff --git a/tools/aapt2/configuration/ConfigurationParser.cpp b/tools/aapt2/configuration/ConfigurationParser.cpp new file mode 100644 index 0000000000000..89618d3a46762 --- /dev/null +++ b/tools/aapt2/configuration/ConfigurationParser.cpp @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "configuration/ConfigurationParser.h" + +#include +#include +#include +#include + +#include + +#include "ConfigDescription.h" +#include "Diagnostics.h" +#include "util/Util.h" +#include "xml/XmlActionExecutor.h" +#include "xml/XmlDom.h" +#include "xml/XmlUtil.h" + +namespace aapt { + +namespace { + +using ::aapt::configuration::Abi; +using ::aapt::configuration::AndroidManifest; +using ::aapt::configuration::AndroidSdk; +using ::aapt::configuration::Artifact; +using ::aapt::configuration::Configuration; +using ::aapt::configuration::GlTexture; +using ::aapt::configuration::Group; +using ::aapt::configuration::Locale; +using ::aapt::util::TrimWhitespace; +using ::aapt::xml::Element; +using ::aapt::xml::FindRootElement; +using ::aapt::xml::NodeCast; +using ::aapt::xml::XmlActionExecutor; +using ::aapt::xml::XmlActionExecutorPolicy; +using ::aapt::xml::XmlNodeAction; + +const std::unordered_map kAbiMap = { + {"armeabi", Abi::kArmeV6}, + {"armeabi-v7a", Abi::kArmV7a}, + {"arm64-v8a", Abi::kArm64V8a}, + {"x86", Abi::kX86}, + {"x86_64", Abi::kX86_64}, + {"mips", Abi::kMips}, + {"mips64", Abi::kMips64}, + {"universal", Abi::kUniversal}, +}; + +constexpr const char* kAaptXmlNs = "http://schemas.android.com/tools/aapt"; + +/** A default noop diagnostics context. */ +class NoopDiagnostics : public IDiagnostics { + public: + void Log(Level level, DiagMessageActual& actualMsg) override {} +}; +NoopDiagnostics noop_; + +std::string GetLabel(const Element* element, IDiagnostics* diag) { + std::string label; + for (const auto& attr : element->attributes) { + if (attr.name == "label") { + label = attr.value; + break; + } + } + + if (label.empty()) { + diag->Error(DiagMessage() << "No label found for element " << element->name); + } + return label; +} + +/** XML node visitor that removes all of the namespace URIs from the node and all children. */ +class NamespaceVisitor : public xml::Visitor { + public: + void Visit(xml::Element* node) override { + node->namespace_uri.clear(); + VisitChildren(node); + } +}; + +} // namespace + +ConfigurationParser::ConfigurationParser(std::string contents) + : contents_(std::move(contents)), + diag_(&noop_) { +} + +Maybe ConfigurationParser::Parse() { + std::istringstream in(contents_); + + auto doc = xml::Inflate(&in, diag_, Source("config.xml")); + if (!doc) { + return {}; + } + + // Strip any namespaces from the XML as the XmlActionExecutor ignores anything with a namespace. + auto* root = FindRootElement(doc.get()); + if (root == nullptr) { + diag_->Error(DiagMessage() << "Could not find the root element in the XML document"); + return {}; + } + + std::string& xml_ns = root->namespace_uri; + if (!xml_ns.empty()) { + if (xml_ns != kAaptXmlNs) { + diag_->Error(DiagMessage() << "Unknown namespace found on root element: " << xml_ns); + return {}; + } + + xml_ns.clear(); + NamespaceVisitor visitor; + root->Accept(&visitor); + } + + XmlActionExecutor executor; + XmlNodeAction& root_action = executor["post-process"]; + XmlNodeAction& artifacts_action = root_action["artifacts"]; + XmlNodeAction& groups_action = root_action["groups"]; + + Configuration config; + + // Helper to bind a static method to an action handler in the DOM executor. + auto bind_handler = [&config](std::function h) + -> XmlNodeAction::ActionFuncWithDiag { + return std::bind(h, &config, std::placeholders::_1, std::placeholders::_2); + }; + + // Parse the artifact elements. + artifacts_action["artifact"].Action(bind_handler(artifact_handler_)); + artifacts_action["artifact-format"].Action(bind_handler(artifact_format_handler_)); + + // Parse the different configuration groups. + groups_action["abi-group"].Action(bind_handler(abi_group_handler_)); + groups_action["screen-density-group"].Action(bind_handler(screen_density_group_handler_)); + groups_action["locale-group"].Action(bind_handler(locale_group_handler_)); + groups_action["android-sdk-group"].Action(bind_handler(android_sdk_group_handler_)); + groups_action["gl-texture-group"].Action(bind_handler(gl_texture_group_handler_)); + groups_action["device-feature-group"].Action(bind_handler(device_feature_group_handler_)); + + if (!executor.Execute(XmlActionExecutorPolicy::kNone, diag_, doc.get())) { + diag_->Error(DiagMessage() << "Could not process XML document"); + return {}; + } + + return {config}; +} + +ConfigurationParser::ActionHandler ConfigurationParser::artifact_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + Artifact artifact{}; + for (const auto& attr : root_element->attributes) { + if (attr.name == "name") { + artifact.name = attr.value; + } else if (attr.name == "abi-group") { + artifact.abi_group = {attr.value}; + } else if (attr.name == "screen-density-group") { + artifact.screen_density_group = {attr.value}; + } else if (attr.name == "locale-group") { + artifact.locale_group = {attr.value}; + } else if (attr.name == "android-sdk-group") { + artifact.android_sdk_group = {attr.value}; + } else if (attr.name == "gl-texture-group") { + artifact.gl_texture_group = {attr.value}; + } else if (attr.name == "device-feature-group") { + artifact.device_feature_group = {attr.value}; + } else { + diag->Note( + DiagMessage() << "Unknown artifact attribute: " << attr.name << " = " << attr.value); + } + } + config->artifacts[artifact.name] = artifact; + return true; + }; + +ConfigurationParser::ActionHandler ConfigurationParser::artifact_format_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + for (auto& node : root_element->children) { + xml::Text* t; + if ((t = NodeCast(node.get())) != nullptr) { + config->artifact_format = TrimWhitespace(t->text).to_string(); + break; + } + } + return true; + }; + +ConfigurationParser::ActionHandler ConfigurationParser::abi_group_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + std::string label = GetLabel(root_element, diag); + if (label.empty()) { + return false; + } + + auto& group = config->abi_groups[label]; + bool valid = true; + + for (auto* child : root_element->GetChildElements()) { + if (child->name != "abi") { + diag->Error( + DiagMessage() << "Unexpected element in ABI group: " << child->name); + valid = false; + } else { + for (auto& node : child->children) { + xml::Text* t; + if ((t = NodeCast(node.get())) != nullptr) { + group.push_back(kAbiMap.at(TrimWhitespace(t->text).to_string())); + break; + } + } + } + } + + return valid; + }; + +ConfigurationParser::ActionHandler ConfigurationParser::screen_density_group_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + std::string label = GetLabel(root_element, diag); + if (label.empty()) { + return false; + } + + auto& group = config->screen_density_groups[label]; + bool valid = true; + + for (auto* child : root_element->GetChildElements()) { + if (child->name != "screen-density") { + diag->Error( + DiagMessage() << "Unexpected root_element in screen density group: " + << child->name); + valid = false; + } else { + for (auto& node : child->children) { + xml::Text* t; + if ((t = NodeCast(node.get())) != nullptr) { + ConfigDescription config_descriptor; + const android::StringPiece& text = TrimWhitespace(t->text); + if (ConfigDescription::Parse(text, &config_descriptor)) { + // Copy the density with the minimum SDK version stripped out. + group.push_back(config_descriptor.CopyWithoutSdkVersion()); + } else { + diag->Error( + DiagMessage() << "Could not parse config descriptor for screen-density: " + << text); + valid = false; + } + break; + } + } + } + } + + return valid; + }; + +ConfigurationParser::ActionHandler ConfigurationParser::locale_group_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + std::string label = GetLabel(root_element, diag); + if (label.empty()) { + return false; + } + + auto& group = config->locale_groups[label]; + bool valid = true; + + for (auto* child : root_element->GetChildElements()) { + if (child->name != "locale") { + diag->Error( + DiagMessage() << "Unexpected root_element in screen density group: " + << child->name); + valid = false; + } else { + Locale entry; + for (const auto& attr : child->attributes) { + if (attr.name == "lang") { + entry.lang = {attr.value}; + } else if (attr.name == "region") { + entry.region = {attr.value}; + } else { + diag->Warn(DiagMessage() << "Unknown attribute: " << attr.name + << " = " << attr.value); + } + } + group.push_back(entry); + } + } + + return valid; + }; + +ConfigurationParser::ActionHandler ConfigurationParser::android_sdk_group_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + std::string label = GetLabel(root_element, diag); + if (label.empty()) { + return false; + } + + auto& group = config->android_sdk_groups[label]; + bool valid = true; + + for (auto* child : root_element->GetChildElements()) { + if (child->name != "android-sdk") { + diag->Error( + DiagMessage() << "Unexpected root_element in ABI group: " << child->name); + valid = false; + } else { + AndroidSdk entry; + for (const auto& attr : child->attributes) { + if (attr.name == "minSdkVersion") { + entry.min_sdk_version = {attr.value}; + } else if (attr.name == "targetSdkVersion") { + entry.target_sdk_version = {attr.value}; + } else if (attr.name == "maxSdkVersion") { + entry.max_sdk_version = {attr.value}; + } else { + diag->Warn(DiagMessage() << "Unknown attribute: " << attr.name + << " = " << attr.value); + } + } + + // TODO: Fill in the manifest details when they are finalised. + for (auto node : child->GetChildElements()) { + if (node->name == "manifest") { + if (entry.manifest) { + diag->Warn(DiagMessage() << "Found multiple manifest tags. Ignoring duplicates."); + continue; + } + entry.manifest = {AndroidManifest()}; + } + } + + group.push_back(entry); + } + } + + return valid; + }; + +ConfigurationParser::ActionHandler ConfigurationParser::gl_texture_group_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + std::string label = GetLabel(root_element, diag); + if (label.empty()) { + return false; + } + + auto& group = config->gl_texture_groups[label]; + bool valid = true; + + GlTexture result; + for (auto* child : root_element->GetChildElements()) { + if (child->name != "gl-texture") { + diag->Error( + DiagMessage() << "Unexpected element in GL texture group: " + << child->name); + valid = false; + } else { + for (const auto& attr : child->attributes) { + if (attr.name == "name") { + result.name = attr.value; + break; + } + } + + for (auto* element : child->GetChildElements()) { + if (element->name != "texture-path") { + diag->Error( + DiagMessage() << "Unexpected element in gl-texture element: " + << child->name); + valid = false; + continue; + } + for (auto& node : element->children) { + xml::Text* t; + if ((t = NodeCast(node.get())) != nullptr) { + result.texture_paths.push_back(TrimWhitespace(t->text).to_string()); + } + } + } + } + group.push_back(result); + } + + return valid; + }; + +ConfigurationParser::ActionHandler ConfigurationParser::device_feature_group_handler_ = + [](Configuration* config, Element* root_element, IDiagnostics* diag) -> bool { + std::string label = GetLabel(root_element, diag); + if (label.empty()) { + return false; + } + + auto& group = config->device_feature_groups[label]; + bool valid = true; + + for (auto* child : root_element->GetChildElements()) { + if (child->name != "supports-feature") { + diag->Error( + DiagMessage() << "Unexpected root_element in device feature group: " + << child->name); + valid = false; + } else { + for (auto& node : child->children) { + xml::Text* t; + if ((t = NodeCast(node.get())) != nullptr) { + group.push_back(TrimWhitespace(t->text).to_string()); + break; + } + } + } + } + + return valid; + }; + +} // namespace aapt diff --git a/tools/aapt2/configuration/ConfigurationParser.h b/tools/aapt2/configuration/ConfigurationParser.h new file mode 100644 index 0000000000000..0fb2f714a76c9 --- /dev/null +++ b/tools/aapt2/configuration/ConfigurationParser.h @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT2_CONFIGURATION_H +#define AAPT2_CONFIGURATION_H + +#include +#include +#include +#include + +#include "util/Maybe.h" + +namespace aapt { + +namespace configuration { + +/** A mapping of group labels to group of configuration items. */ +template +using Group = std::unordered_map>; + +/** Output artifact configuration options. */ +struct Artifact { + /** Name to use for output of processing foo.apk -> foo..apk. */ + std::string name; + /** If present, uses the ABI group with this name. */ + Maybe abi_group; + /** If present, uses the screen density group with this name. */ + Maybe screen_density_group; + /** If present, uses the locale group with this name. */ + Maybe locale_group; + /** If present, uses the Android SDK group with this name. */ + Maybe android_sdk_group; + /** If present, uses the device feature group with this name. */ + Maybe device_feature_group; + /** If present, uses the OpenGL texture group with this name. */ + Maybe gl_texture_group; +}; + +/** Enumeration of currently supported ABIs. */ +enum class Abi { + kArmeV6, + kArmV7a, + kArm64V8a, + kX86, + kX86_64, + kMips, + kMips64, + kUniversal +}; + +/** + * Represents an individual locale. When a locale is included, it must be + * declared from least specific to most specific, as a region does not make + * sense without a language. If neither the language or region are specified it + * acts as a special case for catch all. This can allow all locales to be kept, + * or compressed. + */ +struct Locale { + /** The ISO standard locale language code. */ + Maybe lang; + /** The ISO standard locale region code. */ + Maybe region; + + inline friend bool operator==(const Locale& lhs, const Locale& rhs) { + return lhs.lang == rhs.lang && lhs.region == rhs.region; + } +}; + +// TODO: Encapsulate manifest modifications from the configuration file. +struct AndroidManifest { + inline friend bool operator==(const AndroidManifest& lhs, const AndroidManifest& rhs) { + return true; // nothing to compare yet. + } +}; + +struct AndroidSdk { + Maybe min_sdk_version; + Maybe target_sdk_version; + Maybe max_sdk_version; + Maybe manifest; + + inline friend bool operator==(const AndroidSdk& lhs, const AndroidSdk& rhs) { + return lhs.min_sdk_version == rhs.min_sdk_version && + lhs.target_sdk_version == rhs.target_sdk_version && + lhs.max_sdk_version == rhs.max_sdk_version && + lhs.manifest == rhs.manifest; + } +}; + +// TODO: Make device features more than just an arbitrary string? +using DeviceFeature = std::string; + +/** Represents a mapping of texture paths to a GL texture format. */ +struct GlTexture { + std::string name; + std::vector texture_paths; + + inline friend bool operator==(const GlTexture& lhs, const GlTexture& rhs) { + return lhs.name == rhs.name && lhs.texture_paths == rhs.texture_paths; + } +}; + +/** + * AAPT2 XML configuration binary representation. + */ +struct Configuration { + std::unordered_map artifacts; + Maybe artifact_format; + + Group abi_groups; + Group screen_density_groups; + Group locale_groups; + Group android_sdk_groups; + Group device_feature_groups; + Group gl_texture_groups; +}; + +} // namespace configuration + +// Forward declaration of classes used in the API. +struct IDiagnostics; +namespace xml { +class Element; +} + +/** + * XML configuration file parser for the split and optimize commands. + */ +class ConfigurationParser { + public: + /** Returns a ConfigurationParser for the configuration in the provided file contents. */ + static ConfigurationParser ForContents(const std::string& contents) { + ConfigurationParser parser{contents}; + return parser; + } + + /** Returns a ConfigurationParser for the file located at the provided path. */ + static ConfigurationParser ForPath(const std::string& path) { + // TODO: Read XML file into memory. + return ForContents(path); + } + + /** Sets the diagnostics context to use when parsing. */ + ConfigurationParser& WithDiagnostics(IDiagnostics* diagnostics) { + diag_ = diagnostics; + return *this; + } + + /** + * Parses the configuration file and returns the results. If the configuration could not be parsed + * the result is empty and any errors will be displayed with the provided diagnostics context. + */ + Maybe Parse(); + + protected: + /** + * Instantiates a new ConfigurationParser with the provided configuration file and a no-op + * diagnostics context. The default diagnostics context can be overridden with a call to + * WithDiagnostics(IDiagnostics *). + */ + explicit ConfigurationParser(std::string contents); + + /** Returns the current diagnostics context to any subclasses. */ + IDiagnostics* diagnostics() { + return diag_; + } + + /** + * An ActionHandler for processing XML elements in the XmlActionExecutor. Returns true if the + * element was successfully processed, otherwise returns false. + */ + using ActionHandler = std::function; + + /** Handler for tags. */ + static ActionHandler artifact_handler_; + /** Handler for tags. */ + static ActionHandler artifact_format_handler_; + /** Handler for tags. */ + static ActionHandler abi_group_handler_; + /** Handler for tags. */ + static ActionHandler screen_density_group_handler_; + /** Handler for tags. */ + static ActionHandler locale_group_handler_; + /** Handler for tags. */ + static ActionHandler android_sdk_group_handler_; + /** Handler for tags. */ + static ActionHandler gl_texture_group_handler_; + /** Handler for tags. */ + static ActionHandler device_feature_group_handler_; + + private: + /** The contents of the configuration file to parse. */ + const std::string contents_; + /** The diagnostics context to send messages to. */ + IDiagnostics* diag_; +}; + +} // namespace aapt + +#endif //AAPT2_CONFIGURATION_H diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp new file mode 100644 index 0000000000000..72a97b273cb09 --- /dev/null +++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "configuration/ConfigurationParser.h" + +#include + +#include +#include + +#include "androidfw/ResourceTypes.h" + +#include "test/Test.h" +#include "xml/XmlDom.h" + +namespace aapt { +namespace { + +using android::ResTable_config; +using configuration::Abi; +using configuration::AndroidSdk; +using configuration::Configuration; +using configuration::DeviceFeature; +using configuration::GlTexture; +using configuration::Locale; +using configuration::AndroidManifest; +using testing::ElementsAre; +using xml::Element; +using xml::NodeCast; + +constexpr const char* kValidConfig = R"( + + + + armeabi-v7a + arm64-v8a + + + x86 + mips + + + xhdpi + xxhdpi + xxxhdpi + + + ldpi + mdpi + hdpi + xhdpi + xxhdpi + xxxhdpi + + + + + + + + + + + + + + + + + + + + + + + + + assets/dxt1/* + + + + android.hardware.audio.low_latency + + + + + ${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release + + + + + +)"; + +class ConfigurationParserTest : public ConfigurationParser, public ::testing::Test { + public: + ConfigurationParserTest() : ConfigurationParser("") {} + + protected: + StdErrDiagnostics diag_; +}; + +TEST_F(ConfigurationParserTest, ValidateFile) { + auto parser = ConfigurationParser::ForContents(kValidConfig).WithDiagnostics(&diag_); + auto result = parser.Parse(); + ASSERT_TRUE(result); + Configuration& value = result.value(); + EXPECT_EQ(2ul, value.artifacts.size()); + ASSERT_TRUE(value.artifact_format); + EXPECT_EQ( + "${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release", + value.artifact_format.value() + ); + + EXPECT_EQ(2ul, value.abi_groups.size()); + EXPECT_EQ(2ul, value.abi_groups["arm"].size()); + EXPECT_EQ(2ul, value.abi_groups["other"].size()); + + EXPECT_EQ(2ul, value.screen_density_groups.size()); + EXPECT_EQ(3ul, value.screen_density_groups["large"].size()); + EXPECT_EQ(6ul, value.screen_density_groups["alldpi"].size()); + + EXPECT_EQ(3ul, value.locale_groups.size()); + EXPECT_EQ(4ul, value.locale_groups["europe"].size()); + EXPECT_EQ(3ul, value.locale_groups["north-america"].size()); + EXPECT_EQ(1ul, value.locale_groups["all"].size()); + + EXPECT_EQ(1ul, value.android_sdk_groups.size()); + EXPECT_EQ(1ul, value.android_sdk_groups["19"].size()); + + EXPECT_EQ(1ul, value.gl_texture_groups.size()); + EXPECT_EQ(1ul, value.gl_texture_groups["dxt1"].size()); + + EXPECT_EQ(1ul, value.device_feature_groups.size()); + EXPECT_EQ(1ul, value.device_feature_groups["low-latency"].size()); +} + +TEST_F(ConfigurationParserTest, InvalidNamespace) { + constexpr const char* invalid_ns = R"( + )"; + + auto result = ConfigurationParser::ForContents(invalid_ns).Parse(); + ASSERT_FALSE(result); +} + +TEST_F(ConfigurationParserTest, ArtifactAction) { + static constexpr const char* xml = R"xml( + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok = artifact_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + EXPECT_EQ(1ul, config.artifacts.size()); + + auto& artifact = config.artifacts.begin()->second; + EXPECT_EQ("", artifact.name); // TODO: make this fail. + EXPECT_EQ("arm", artifact.abi_group.value()); + EXPECT_EQ("large", artifact.screen_density_group.value()); + EXPECT_EQ("europe", artifact.locale_group.value()); + EXPECT_EQ("19", artifact.android_sdk_group.value()); + EXPECT_EQ("dxt1", artifact.gl_texture_group.value()); + EXPECT_EQ("low-latency", artifact.device_feature_group.value()); +} + +TEST_F(ConfigurationParserTest, ArtifactFormatAction) { + static constexpr const char* xml = R"xml( + + ${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok = artifact_format_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + ASSERT_TRUE(config.artifact_format); + EXPECT_EQ( + "${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release", + static_cast(config.artifact_format.value()) + ); +} + +TEST_F(ConfigurationParserTest, AbiGroupAction) { + static constexpr const char* xml = R"xml( + + + + armeabi-v7a + + + arm64-v8a + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok = abi_group_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + EXPECT_EQ(1ul, config.abi_groups.size()); + ASSERT_EQ(1u, config.abi_groups.count("arm")); + + auto& out = config.abi_groups["arm"]; + ASSERT_THAT(out, ElementsAre(Abi::kArmV7a, Abi::kArm64V8a)); +} + +TEST_F(ConfigurationParserTest, ScreenDensityGroupAction) { + static constexpr const char* xml = R"xml( + + xhdpi + + xxhdpi + + xxxhdpi + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok = + screen_density_group_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + EXPECT_EQ(1ul, config.screen_density_groups.size()); + ASSERT_EQ(1u, config.screen_density_groups.count("large")); + + ConfigDescription xhdpi; + xhdpi.density = ResTable_config::DENSITY_XHIGH; + ConfigDescription xxhdpi; + xxhdpi.density = ResTable_config::DENSITY_XXHIGH; + ConfigDescription xxxhdpi; + xxxhdpi.density = ResTable_config::DENSITY_XXXHIGH; + + auto& out = config.screen_density_groups["large"]; + ASSERT_THAT(out, ElementsAre(xhdpi, xxhdpi, xxxhdpi)); +} + +TEST_F(ConfigurationParserTest, LocaleGroupAction) { + static constexpr const char* xml = R"xml( + + + + + + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok = locale_group_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + ASSERT_EQ(1ul, config.locale_groups.size()); + ASSERT_EQ(1u, config.locale_groups.count("europe")); + + auto& out = config.locale_groups["europe"]; + + Locale en; + en.lang = std::string("en"); + Locale es; + es.lang = std::string("es"); + Locale fr; + fr.lang = std::string("fr"); + Locale de; + de.lang = std::string("de"); + + ASSERT_THAT(out, ElementsAre(en, es, fr, de)); +} + +TEST_F(ConfigurationParserTest, AndroidSdkGroupAction) { + static constexpr const char* xml = R"xml( + + + + + + + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok = android_sdk_group_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + ASSERT_EQ(1ul, config.android_sdk_groups.size()); + ASSERT_EQ(1u, config.android_sdk_groups.count("19")); + + auto& out = config.android_sdk_groups["19"]; + + AndroidSdk sdk; + sdk.min_sdk_version = std::string("19"); + sdk.target_sdk_version = std::string("24"); + sdk.max_sdk_version = std::string("25"); + sdk.manifest = AndroidManifest(); + + ASSERT_EQ(1ul, out.size()); + ASSERT_EQ(sdk, out[0]); +} + +TEST_F(ConfigurationParserTest, GlTextureGroupAction) { + static constexpr const char* xml = R"xml( + + + assets/dxt1/main/* + + assets/dxt1/test/* + + + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok = gl_texture_group_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + EXPECT_EQ(1ul, config.gl_texture_groups.size()); + ASSERT_EQ(1u, config.gl_texture_groups.count("dxt1")); + + auto& out = config.gl_texture_groups["dxt1"]; + + GlTexture texture{ + std::string("GL_EXT_texture_compression_dxt1"), + {"assets/dxt1/main/*", "assets/dxt1/test/*"} + }; + + ASSERT_EQ(1ul, out.size()); + ASSERT_EQ(texture, out[0]); +} + +TEST_F(ConfigurationParserTest, DeviceFeatureGroupAction) { + static constexpr const char* xml = R"xml( + + android.hardware.audio.low_latency + + android.hardware.audio.pro + + )xml"; + + auto doc = test::BuildXmlDom(xml); + + Configuration config; + bool ok + = device_feature_group_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + EXPECT_EQ(1ul, config.device_feature_groups.size()); + ASSERT_EQ(1u, config.device_feature_groups.count("low-latency")); + + auto& out = config.device_feature_groups["low-latency"]; + + DeviceFeature low_latency = "android.hardware.audio.low_latency"; + DeviceFeature pro = "android.hardware.audio.pro"; + ASSERT_THAT(out, ElementsAre(low_latency, pro)); +} + +} // namespace +} // namespace aapt