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 {