diff options
-rw-r--r-- | tools/aapt2/Android.bp | 1 | ||||
-rw-r--r-- | tools/aapt2/ResourceParser.cpp | 7 | ||||
-rw-r--r-- | tools/aapt2/cmd/Link.cpp | 12 | ||||
-rw-r--r-- | tools/aapt2/link/FeatureFlagsFilter.cpp | 4 | ||||
-rw-r--r-- | tools/aapt2/link/FlaggedResources_test.cpp | 2 | ||||
-rw-r--r-- | tools/aapt2/link/FlaggedXmlVersioner.cpp | 92 | ||||
-rw-r--r-- | tools/aapt2/link/FlaggedXmlVersioner.h | 42 | ||||
-rw-r--r-- | tools/aapt2/link/FlaggedXmlVersioner_test.cpp | 220 | ||||
-rw-r--r-- | tools/aapt2/xml/XmlUtil.h | 1 |
9 files changed, 374 insertions, 7 deletions
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index f43cf521edf5..43d5b7165f06 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -113,6 +113,7 @@ cc_library_host_static { "io/ZipArchive.cpp", "link/AutoVersioner.cpp", "link/FeatureFlagsFilter.cpp", + "link/FlaggedXmlVersioner.cpp", "link/FlagDisabledResourceRemover.cpp", "link/ManifestFixer.cpp", "link/NoDefaultResourceRemover.cpp", diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index fb576df248be..9e2a4c1b1cc2 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -547,7 +547,8 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, }); std::string_view resource_type = parser->element_name(); - if (auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"))) { + if (auto flag = + ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, xml::kAttrFeatureFlag))) { if (options_.flag) { diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << "Resource flag are not allowed both in the path and in the file"); @@ -1529,7 +1530,7 @@ bool ResourceParser::ParseStyleItem(xml::XmlPullParser* parser, Style* style) { ResolvePackage(parser, &maybe_key.value()); maybe_key.value().SetSource(source); - auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag")); + auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, xml::kAttrFeatureFlag)); std::unique_ptr<Item> value = ParseXml(parser, 0, kAllowRawString); if (!value) { @@ -1674,7 +1675,7 @@ bool ResourceParser::ParseArrayImpl(xml::XmlPullParser* parser, const std::string& element_namespace = parser->element_namespace(); const std::string& element_name = parser->element_name(); if (element_namespace.empty() && element_name == "item") { - auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag")); + auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, xml::kAttrFeatureFlag)); std::unique_ptr<Item> item = ParseXml(parser, typeMask, kNoRawString); if (!item) { diag_->Error(android::DiagMessage(item_source) << "could not parse array item"); diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 0a5cb1ff4956..2a7921600477 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -58,6 +58,7 @@ #include "java/ProguardRules.h" #include "link/FeatureFlagsFilter.h" #include "link/FlagDisabledResourceRemover.h" +#include "link/FlaggedXmlVersioner.h" #include "link/Linkers.h" #include "link/ManifestFixer.h" #include "link/NoDefaultResourceRemover.h" @@ -503,10 +504,19 @@ std::vector<std::unique_ptr<xml::XmlResource>> ResourceFileFlattener::LinkAndVer const ConfigDescription& config = file_op->config; ResourceEntry* entry = file_op->entry; + FlaggedXmlVersioner flagged_xml_versioner; + auto flag_split_resources = flagged_xml_versioner.Process(context_, doc); + + std::vector<std::unique_ptr<xml::XmlResource>> final_resources; XmlCompatVersioner xml_compat_versioner(&rules_); const util::Range<ApiVersion> api_range{config.sdkVersion, FindNextApiVersionForConfig(entry, config)}; - return xml_compat_versioner.Process(context_, doc, api_range); + for (auto& split_res : flag_split_resources) { + auto inner_resources = xml_compat_versioner.Process(context_, split_res.get(), api_range); + final_resources.insert(final_resources.end(), std::make_move_iterator(inner_resources.begin()), + std::make_move_iterator(inner_resources.end())); + } + return final_resources; } ResourceFile::Type XmlFileTypeForOutputFormat(OutputFormat format) { diff --git a/tools/aapt2/link/FeatureFlagsFilter.cpp b/tools/aapt2/link/FeatureFlagsFilter.cpp index 23f78388b930..74066a37e8ac 100644 --- a/tools/aapt2/link/FeatureFlagsFilter.cpp +++ b/tools/aapt2/link/FeatureFlagsFilter.cpp @@ -51,7 +51,7 @@ class FlagsVisitor : public xml::Visitor { private: bool ShouldRemove(std::unique_ptr<xml::Node>& node) { if (auto* el = NodeCast<Element>(node.get())) { - auto* attr = el->FindAttribute(xml::kSchemaAndroid, "featureFlag"); + auto* attr = el->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); if (attr == nullptr) { return false; } @@ -76,7 +76,7 @@ class FlagsVisitor : public xml::Visitor { // Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag" bool remove = *it->second.enabled == negated; if (!remove) { - el->RemoveAttribute(xml::kSchemaAndroid, "featureFlag"); + el->RemoveAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); } return remove; } diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp index 7bea96c26990..dbef77615515 100644 --- a/tools/aapt2/link/FlaggedResources_test.cpp +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -163,7 +163,7 @@ TEST_F(FlaggedResourcesTest, EnabledXmlELementAttributeRemoved) { auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); std::string output; - DumpXmlTreeToString(loaded_apk.get(), "res/layout-v22/layout1.xml", &output); + DumpXmlTreeToString(loaded_apk.get(), "res/layout-v36/layout1.xml", &output); ASSERT_FALSE(output.contains("test.package.trueFlag")); ASSERT_TRUE(output.contains("FIND_ME")); ASSERT_TRUE(output.contains("test.package.readWriteFlag")); diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp new file mode 100644 index 000000000000..75c6f17dcb51 --- /dev/null +++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 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 "link/FlaggedXmlVersioner.h" + +#include "SdkConstants.h" +#include "androidfw/Util.h" + +using ::aapt::xml::Element; +using ::aapt::xml::NodeCast; + +namespace aapt { + +// An xml visitor that goes through the a doc and removes any elements that are behind non-negated +// flags. It also removes the featureFlag attribute from elements behind negated flags. +class AllDisabledFlagsVisitor : public xml::Visitor { + public: + void Visit(xml::Element* node) override { + std::erase_if(node->children, [this](const std::unique_ptr<xml::Node>& node) { + return FixupOrShouldRemove(node); + }); + VisitChildren(node); + } + + bool HadFlags() const { + return had_flags_; + } + + private: + bool FixupOrShouldRemove(const std::unique_ptr<xml::Node>& node) { + if (auto* el = NodeCast<Element>(node.get())) { + auto* attr = el->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); + if (attr == nullptr) { + return false; + } + + had_flags_ = true; + // This class assumes all flags are disabled so we want to remove any elements behind flags + // unless the flag specification is negated. In the negated case we remove the featureFlag + // attribute because we have already determined whether we are keeping the element or not. + std::string_view flag_name = util::TrimWhitespace(attr->value); + if (flag_name.starts_with('!')) { + el->RemoveAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); + return false; + } else { + return true; + } + } + + return false; + } + + bool had_flags_ = false; +}; + +std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context, + xml::XmlResource* doc) { + std::vector<std::unique_ptr<xml::XmlResource>> docs; + if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) || + (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) { + // Support for read/write flags was added in baklava so if the doc will only get used on + // baklava or later we can just return the original doc. + docs.push_back(doc->Clone()); + } else { + auto preBaklavaVersion = doc->Clone(); + AllDisabledFlagsVisitor visitor; + preBaklavaVersion->root->Accept(&visitor); + docs.push_back(std::move(preBaklavaVersion)); + + if (visitor.HadFlags()) { + auto baklavaVersion = doc->Clone(); + baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA; + docs.push_back(std::move(baklavaVersion)); + } + } + return docs; +} + +} // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/link/FlaggedXmlVersioner.h b/tools/aapt2/link/FlaggedXmlVersioner.h new file mode 100644 index 000000000000..44ed266602f6 --- /dev/null +++ b/tools/aapt2/link/FlaggedXmlVersioner.h @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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. + */ + +#pragma once + +#include <memory> +#include <vector> + +#include "process/IResourceTableConsumer.h" +#include "xml/XmlDom.h" + +namespace aapt { + +// FlaggedXmlVersioner takes an XmlResource and checks if any elements have read write android +// flags on them. If the doc doesn't refer to any such flags the returned vector only contains +// the original doc. +// +// Read/write flags within xml resources files is only supported in android baklava and later. If +// the config resource specifies a version that is baklava or later it returns a vector containing +// the original XmlResource. Otherwise FlaggedXmlVersioner creates a version of the doc where all +// flags are assumed disabled and the config version is the same as the original doc, if specified. +// It also creates an XmlResource where the contents are the same as the original doc and the config +// version is baklava. The returned vector is composed of these two new docs. +class FlaggedXmlVersioner { + public: + std::vector<std::unique_ptr<xml::XmlResource>> Process(IAaptContext* context, + xml::XmlResource* doc); +}; +} // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp new file mode 100644 index 000000000000..0c1314f165cc --- /dev/null +++ b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2025 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 "link/FlaggedXmlVersioner.h" + +#include "Debug.h" +#include "SdkConstants.h" +#include "io/StringStream.h" +#include "test/Test.h" + +using ::aapt::test::ValueEq; +using ::testing::Eq; +using ::testing::IsNull; +using ::testing::NotNull; +using ::testing::Pointee; +using ::testing::SizeIs; + +namespace aapt { + +class FlaggedXmlVersionerTest : public ::testing::Test { + public: + void SetUp() override { + context_ = test::ContextBuilder() + .SetCompilationPackage("com.app") + .SetPackageId(0x7f) + .SetPackageType(PackageType::kApp) + .Build(); + } + + protected: + std::unique_ptr<IAaptContext> context_; +}; + +static void PrintDocToString(xml::XmlResource* doc, std::string* out) { + io::StringOutputStream stream(out, 1024u); + text::Printer printer(&stream); + Debug::DumpXml(*doc, &printer); + stream.Flush(); +} + +TEST_F(FlaggedXmlVersionerTest, NoFlagReturnsOriginal) { + auto doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView /> + <TextView /> + <TextView /> + </LinearLayout>)"); + doc->file.config.sdkVersion = SDK_GINGERBREAD; + + FlaggedXmlVersioner versioner; + auto results = versioner.Process(context_.get(), doc.get()); + EXPECT_THAT(results.size(), Eq(1)); + EXPECT_THAT(results[0]->file.config.sdkVersion, Eq(SDK_GINGERBREAD)); + + std::string expected; + PrintDocToString(doc.get(), &expected); + std::string actual; + PrintDocToString(results[0].get(), &actual); + + EXPECT_THAT(actual, Eq(expected)); +} + +TEST_F(FlaggedXmlVersionerTest, AlreadyBaklavaReturnsOriginal) { + auto doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView android:featureFlag="package.flag" /> + <TextView /> + <TextView /> + </LinearLayout>)"); + doc->file.config.sdkVersion = SDK_BAKLAVA; + + FlaggedXmlVersioner versioner; + auto results = versioner.Process(context_.get(), doc.get()); + EXPECT_THAT(results.size(), Eq(1)); + EXPECT_THAT(results[0]->file.config.sdkVersion, Eq(SDK_BAKLAVA)); + + std::string expected; + PrintDocToString(doc.get(), &expected); + std::string actual; + PrintDocToString(results[0].get(), &actual); + + EXPECT_THAT(actual, Eq(expected)); +} + +TEST_F(FlaggedXmlVersionerTest, PreBaklavaGetsSplit) { + auto doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView android:featureFlag="package.flag" /><TextView /><TextView /> + </LinearLayout>)"); + doc->file.config.sdkVersion = SDK_GINGERBREAD; + + FlaggedXmlVersioner versioner; + auto results = versioner.Process(context_.get(), doc.get()); + EXPECT_THAT(results.size(), Eq(2)); + EXPECT_THAT(results[0]->file.config.sdkVersion, Eq(SDK_GINGERBREAD)); + EXPECT_THAT(results[1]->file.config.sdkVersion, Eq(SDK_BAKLAVA)); + + auto gingerbread_doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView /><TextView /> + </LinearLayout>)"); + + std::string expected0; + PrintDocToString(gingerbread_doc.get(), &expected0); + std::string actual0; + PrintDocToString(results[0].get(), &actual0); + EXPECT_THAT(actual0, Eq(expected0)); + + std::string expected1; + PrintDocToString(doc.get(), &expected1); + std::string actual1; + PrintDocToString(results[1].get(), &actual1); + EXPECT_THAT(actual1, Eq(expected1)); +} + +TEST_F(FlaggedXmlVersionerTest, NoVersionGetsSplit) { + auto doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView android:featureFlag="package.flag" /><TextView /><TextView /> + </LinearLayout>)"); + + FlaggedXmlVersioner versioner; + auto results = versioner.Process(context_.get(), doc.get()); + EXPECT_THAT(results.size(), Eq(2)); + EXPECT_THAT(results[0]->file.config.sdkVersion, Eq(0)); + EXPECT_THAT(results[1]->file.config.sdkVersion, Eq(SDK_BAKLAVA)); + + auto gingerbread_doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView /><TextView /> + </LinearLayout>)"); + + std::string expected0; + PrintDocToString(gingerbread_doc.get(), &expected0); + std::string actual0; + PrintDocToString(results[0].get(), &actual0); + EXPECT_THAT(actual0, Eq(expected0)); + + std::string expected1; + PrintDocToString(doc.get(), &expected1); + std::string actual1; + PrintDocToString(results[1].get(), &actual1); + EXPECT_THAT(actual1, Eq(expected1)); +} + +TEST_F(FlaggedXmlVersionerTest, NegatedFlagAttributeRemoved) { + auto doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView android:featureFlag="!package.flag" /><TextView /><TextView /> + </LinearLayout>)"); + doc->file.config.sdkVersion = SDK_GINGERBREAD; + + FlaggedXmlVersioner versioner; + auto results = versioner.Process(context_.get(), doc.get()); + EXPECT_THAT(results.size(), Eq(2)); + EXPECT_THAT(results[0]->file.config.sdkVersion, Eq(SDK_GINGERBREAD)); + EXPECT_THAT(results[1]->file.config.sdkVersion, Eq(SDK_BAKLAVA)); + + auto gingerbread_doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView /><TextView /><TextView /> + </LinearLayout>)"); + + std::string expected0; + PrintDocToString(gingerbread_doc.get(), &expected0); + std::string actual0; + PrintDocToString(results[0].get(), &actual0); + EXPECT_THAT(actual0, Eq(expected0)); + + std::string expected1; + PrintDocToString(doc.get(), &expected1); + std::string actual1; + PrintDocToString(results[1].get(), &actual1); + EXPECT_THAT(actual1, Eq(expected1)); +} + +TEST_F(FlaggedXmlVersionerTest, NegatedFlagAttributeRemovedNoSpecifiedVersion) { + auto doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView android:featureFlag="!package.flag" /><TextView /><TextView /> + </LinearLayout>)"); + + FlaggedXmlVersioner versioner; + auto results = versioner.Process(context_.get(), doc.get()); + EXPECT_THAT(results.size(), Eq(2)); + EXPECT_THAT(results[0]->file.config.sdkVersion, Eq(0)); + EXPECT_THAT(results[1]->file.config.sdkVersion, Eq(SDK_BAKLAVA)); + + auto gingerbread_doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView /><TextView /><TextView /> + </LinearLayout>)"); + + std::string expected0; + PrintDocToString(gingerbread_doc.get(), &expected0); + std::string actual0; + PrintDocToString(results[0].get(), &actual0); + EXPECT_THAT(actual0, Eq(expected0)); + + std::string expected1; + PrintDocToString(doc.get(), &expected1); + std::string actual1; + PrintDocToString(results[1].get(), &actual1); + EXPECT_THAT(actual1, Eq(expected1)); +} + +} // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/xml/XmlUtil.h b/tools/aapt2/xml/XmlUtil.h index ad676ca91886..789f6a05510b 100644 --- a/tools/aapt2/xml/XmlUtil.h +++ b/tools/aapt2/xml/XmlUtil.h @@ -30,6 +30,7 @@ constexpr const char* kSchemaPrivatePrefix = "http://schemas.android.com/apk/prv constexpr const char* kSchemaAndroid = "http://schemas.android.com/apk/res/android"; constexpr const char* kSchemaTools = "http://schemas.android.com/tools"; constexpr const char* kSchemaAapt = "http://schemas.android.com/aapt"; +constexpr const char* kAttrFeatureFlag = "featureFlag"; // Result of extracting a package name from a namespace URI declaration. struct ExtractedPackage { |