Only have featureFlag attr in xml when v>baklava
This makes it so that if there are xml files that use the featureFlag
attribute they are split into a pre-B file where all flags are assumed
false and a B version that is the file as is.
Test: Automation
Bug: 377974898
Flag: android.content.res.layout_readwrite_flags
Change-Id: Iab1a69a6d0b3e7efd7033887c351430fb2aabd19
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index f43cf52..43d5b71 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -113,6 +113,7 @@
"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 fb576df..9e2a4c1 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -547,7 +547,8 @@
});
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 @@
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 @@
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 0a5cb1f..2a79216 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 @@
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 23f7838..74066a3 100644
--- a/tools/aapt2/link/FeatureFlagsFilter.cpp
+++ b/tools/aapt2/link/FeatureFlagsFilter.cpp
@@ -51,7 +51,7 @@
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 @@
// 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 7bea96c..dbef776 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -163,7 +163,7 @@
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 0000000..75c6f17
--- /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 0000000..44ed266
--- /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 0000000..0c1314f
--- /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 ad676ca..789f6a0 100644
--- a/tools/aapt2/xml/XmlUtil.h
+++ b/tools/aapt2/xml/XmlUtil.h
@@ -30,6 +30,7 @@
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 {