First pass at flagged resources

This gets the main parts of resource flagging in place and the basic use
case of flagging with an xml attribute working.

Test: Automated
Bug: 329436914
Flag: EXEMPT Aconfig not supported on host tools
Change-Id: Id2b5ba450d05da00a922e98ca204b6e5aa6c6c24
diff --git a/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp
new file mode 100644
index 0000000..e8bb710
--- /dev/null
+++ b/core/tests/resourceflaggingtests/Android.bp
@@ -0,0 +1,75 @@
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_android_resources",
+}
+
+genrule {
+    name: "resource-flagging-test-app-resources-compile",
+    tools: ["aapt2"],
+    srcs: [
+        "flagged_resources_res/values/bools.xml",
+    ],
+    out: ["values_bools.arsc.flat"],
+    cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
+        "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
+}
+
+genrule {
+    name: "resource-flagging-test-app-apk",
+    tools: ["aapt2"],
+    // The first input file in the list must be the manifest
+    srcs: [
+        "TestAppAndroidManifest.xml",
+        ":resource-flagging-test-app-resources-compile",
+    ],
+    out: ["resapp.apk"],
+    cmd: "$(location aapt2) link -o $(out) --manifest $(in)",
+}
+
+java_genrule {
+    name: "resource-flagging-apk-as-resource",
+    srcs: [
+        ":resource-flagging-test-app-apk",
+    ],
+    out: ["apks_as_resources.res.zip"],
+    tools: ["soong_zip"],
+
+    cmd: "mkdir -p $(genDir)/res/raw && " +
+        "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " +
+        "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res",
+}
+
+android_test {
+    name: "ResourceFlaggingTests",
+    srcs: [
+        "src/**/*.java",
+    ],
+    platform_apis: true,
+    certificate: "platform",
+    static_libs: [
+        "androidx.test.rules",
+        "testng",
+        "compatibility-device-util-axt",
+    ],
+    resource_zips: [":resource-flagging-apk-as-resource"],
+    test_suites: ["device-tests"],
+}
diff --git a/core/tests/resourceflaggingtests/AndroidManifest.xml b/core/tests/resourceflaggingtests/AndroidManifest.xml
new file mode 100644
index 0000000..938463b
--- /dev/null
+++ b/core/tests/resourceflaggingtests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.resourceflaggingtests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.resourceflaggingtests"
+        android:label="Resource Flagging Tests" />
+
+</manifest>
\ No newline at end of file
diff --git a/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml b/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml
new file mode 100644
index 0000000..d6cdeb7
--- /dev/null
+++ b/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml
@@ -0,0 +1,4 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.intenal.flaggedresources">
+    <application/>
+</manifest>
\ No newline at end of file
diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
new file mode 100644
index 0000000..f4defd9
--- /dev/null
+++ b/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <bool name="res1">true</bool>
+    <bool name="res1" android:featureFlag="test.package.falseFlag">false</bool>
+
+    <bool name="res2">false</bool>
+    <bool name="res2" android:featureFlag="test.package.trueFlag">true</bool>
+</resources>
\ No newline at end of file
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
new file mode 100644
index 0000000..a0cbe3c
--- /dev/null
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.resourceflaggingtests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.FileUtils;
+import android.util.DisplayMetrics;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.InputStream;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class ResourceFlaggingTest {
+    private Context mContext;
+    private Resources mResources;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+        AssetManager assets = new AssetManager();
+        assertThat(assets.addAssetPath(extractApkAndGetPath(R.raw.resapp))).isNotEqualTo(0);
+
+        final DisplayMetrics dm = new DisplayMetrics();
+        dm.setToDefaults();
+        mResources = new Resources(assets, dm, new Configuration());
+    }
+
+    @Test
+    public void testFlagDisabled() {
+        assertThat(getBoolean("res1")).isTrue();
+    }
+
+    @Test
+    public void testFlagEnabled() {
+        assertThat(getBoolean("res2")).isTrue();
+    }
+
+    private boolean getBoolean(String name) {
+        int resId = mResources.getIdentifier(name, "bool", "com.android.intenal.flaggedresources");
+        assertThat(resId).isNotEqualTo(0);
+        return mResources.getBoolean(resId);
+    }
+
+    private String extractApkAndGetPath(int id) throws Exception {
+        final Resources resources = mContext.getResources();
+        try (InputStream is = resources.openRawResource(id)) {
+            File path = new File(mContext.getFilesDir(), resources.getResourceEntryName(id));
+            path.deleteOnExit();
+            FileUtils.copyToFileOrThrow(is, path);
+            return path.getAbsolutePath();
+        }
+    }
+}
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index 7ba3277..a274f04 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -69,6 +69,8 @@
   kXml,
 };
 
+enum class FlagStatus { NoFlag = 0, Disabled = 1, Enabled = 2 };
+
 android::StringPiece to_string(ResourceType type);
 
 /**
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index 6af39b7..2df9418 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 #include "ResourceParser.h"
 
 #include <functional>
@@ -108,6 +107,7 @@
   Visibility::Level visibility_level = Visibility::Level::kUndefined;
   bool staged_api = false;
   bool allow_new = false;
+  FlagStatus flag_status;
   std::optional<OverlayableItem> overlayable_item;
   std::optional<StagedId> staged_alias;
 
@@ -161,6 +161,8 @@
     res_builder.SetStagedId(res->staged_alias.value());
   }
 
+  res_builder.SetFlagStatus(res->flag_status);
+
   bool error = false;
   if (!res->name.entry.empty()) {
     if (!table->AddResource(res_builder.Build(), diag)) {
@@ -544,6 +546,30 @@
   });
 
   std::string resource_type = parser->element_name();
+  std::optional<StringPiece> flag =
+      xml::FindAttribute(parser, "http://schemas.android.com/apk/res/android", "featureFlag");
+  out_resource->flag_status = FlagStatus::NoFlag;
+  if (flag) {
+    auto flag_it = options_.feature_flag_values.find(flag.value());
+    if (flag_it == options_.feature_flag_values.end()) {
+      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
+                   << "Resource flag value undefined");
+      return false;
+    }
+    const auto& flag_properties = flag_it->second;
+    if (!flag_properties.read_only) {
+      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
+                   << "Only read only flags may be used with resources");
+      return false;
+    }
+    if (!flag_properties.enabled.has_value()) {
+      diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
+                   << "Only flags with a value may be used with resources");
+      return false;
+    }
+    out_resource->flag_status =
+        flag_properties.enabled.value() ? FlagStatus::Enabled : FlagStatus::Disabled;
+  }
 
   // The value format accepted for this resource.
   uint32_t resource_format = 0u;
diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h
index 012a056..45d41c1 100644
--- a/tools/aapt2/ResourceParser.h
+++ b/tools/aapt2/ResourceParser.h
@@ -27,6 +27,7 @@
 #include "androidfw/IDiagnostics.h"
 #include "androidfw/StringPiece.h"
 #include "androidfw/StringPool.h"
+#include "cmd/Util.h"
 #include "xml/XmlPullParser.h"
 
 namespace aapt {
@@ -54,6 +55,8 @@
   // If visibility was forced, we need to use it when creating a new resource and also error if we
   // try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags.
   std::optional<Visibility::Level> visibility;
+
+  FeatureFlagValues feature_flag_values;
 };
 
 struct FlattenedXmlSubTree {
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index a3b0b45..1cdb715 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -231,6 +231,47 @@
   return false;
 }
 
+ResourceTable::CollisionResult ResourceTable::ResolveFlagCollision(FlagStatus existing,
+                                                                   FlagStatus incoming) {
+  switch (existing) {
+    case FlagStatus::NoFlag:
+      switch (incoming) {
+        case FlagStatus::NoFlag:
+          return CollisionResult::kConflict;
+        case FlagStatus::Disabled:
+          return CollisionResult::kKeepOriginal;
+        case FlagStatus::Enabled:
+          return CollisionResult::kTakeNew;
+        default:
+          return CollisionResult::kConflict;
+      }
+    case FlagStatus::Disabled:
+      switch (incoming) {
+        case FlagStatus::NoFlag:
+          return CollisionResult::kTakeNew;
+        case FlagStatus::Disabled:
+          return CollisionResult::kKeepOriginal;
+        case FlagStatus::Enabled:
+          return CollisionResult::kTakeNew;
+        default:
+          return CollisionResult::kConflict;
+      }
+    case FlagStatus::Enabled:
+      switch (incoming) {
+        case FlagStatus::NoFlag:
+          return CollisionResult::kKeepOriginal;
+        case FlagStatus::Disabled:
+          return CollisionResult::kKeepOriginal;
+        case FlagStatus::Enabled:
+          return CollisionResult::kConflict;
+        default:
+          return CollisionResult::kConflict;
+      }
+    default:
+      return CollisionResult::kConflict;
+  }
+}
+
 // The default handler for collisions.
 //
 // Typically, a weak value will be overridden by a strong value. An existing weak
@@ -564,16 +605,21 @@
     if (!config_value->value) {
       // Resource does not exist, add it now.
       config_value->value = std::move(res.value);
+      config_value->flag_status = res.flag_status;
     } else {
       // When validation is enabled, ensure that a resource cannot have multiple values defined for
-      // the same configuration.
-      auto result = validate ? ResolveValueCollision(config_value->value.get(), res.value.get())
+      // the same configuration unless protected by flags.
+      auto result = validate ? ResolveFlagCollision(config_value->flag_status, res.flag_status)
                              : CollisionResult::kKeepBoth;
+      if (result == CollisionResult::kConflict) {
+        result = ResolveValueCollision(config_value->value.get(), res.value.get());
+      }
       switch (result) {
         case CollisionResult::kKeepBoth:
           // Insert the value ignoring for duplicate configurations
           entry->values.push_back(util::make_unique<ResourceConfigValue>(res.config, res.product));
           entry->values.back()->value = std::move(res.value);
+          entry->values.back()->flag_status = res.flag_status;
           break;
 
         case CollisionResult::kTakeNew:
@@ -735,6 +781,11 @@
   return *this;
 }
 
+NewResourceBuilder& NewResourceBuilder::SetFlagStatus(FlagStatus flag_status) {
+  res_.flag_status = flag_status;
+  return *this;
+}
+
 NewResource NewResourceBuilder::Build() {
   return std::move(res_);
 }
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 61e399c..9530c17 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -104,6 +104,8 @@
   // The actual Value.
   std::unique_ptr<Value> value;
 
+  FlagStatus flag_status;
+
   ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product)
       : config(config), product(product) {
   }
@@ -269,6 +271,7 @@
   std::optional<AllowNew> allow_new;
   std::optional<StagedId> staged_id;
   bool allow_mangled = false;
+  FlagStatus flag_status;
 };
 
 struct NewResourceBuilder {
@@ -282,6 +285,7 @@
   NewResourceBuilder& SetAllowNew(AllowNew allow_new);
   NewResourceBuilder& SetStagedId(StagedId id);
   NewResourceBuilder& SetAllowMangled(bool allow_mangled);
+  NewResourceBuilder& SetFlagStatus(FlagStatus flag_status);
   NewResource Build();
 
  private:
@@ -330,7 +334,8 @@
 
   std::unique_ptr<ResourceTable> Clone() const;
 
-  // When a collision of resources occurs, this method decides which value to keep.
+  // When a collision of resources occurs, these methods decide which value to keep.
+  static CollisionResult ResolveFlagCollision(FlagStatus existing, FlagStatus incoming);
   static CollisionResult ResolveValueCollision(Value* existing, Value* incoming);
 
   // The string pool used by this resource table. Values that reference strings must use
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index 1d7fd1d..2ecc82a 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -246,6 +246,7 @@
 message ConfigValue {
   Configuration config = 1;
   Value value = 2;
+  uint32 flag_status = 3;
 }
 
 // The generic meta-data for every value in a resource table.
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index 9b8c3b3..2a978a5 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -171,6 +171,7 @@
     parser_options.error_on_positional_arguments = !options.legacy_mode;
     parser_options.preserve_visibility_of_styleables = options.preserve_visibility_of_styleables;
     parser_options.translatable = translatable_file;
+    parser_options.feature_flag_values = options.feature_flag_values;
 
     // If visibility was forced, we need to use it when creating a new resource and also error if
     // we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags.
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index e1a3013..aaab315 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -16,6 +16,7 @@
 
 #include "format/proto/ProtoDeserialize.h"
 
+#include "Resource.h"
 #include "ResourceTable.h"
 #include "ResourceUtils.h"
 #include "ResourceValues.h"
@@ -533,6 +534,8 @@
           return false;
         }
 
+        config_value->flag_status = (FlagStatus)pb_config_value.flag_status();
+
         config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
                                                      &out_table->string_pool, files, out_error);
         if (config_value->value == nullptr) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 0903205..c1e15bc 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -426,6 +426,7 @@
           pb_config_value->mutable_config()->set_product(config_value->product);
           SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(),
                              source_pool.get());
+          pb_config_value->set_flag_status((uint32_t)config_value->flag_status);
         }
       }
     }
diff --git a/tools/aapt2/xml/XmlPullParser.cpp b/tools/aapt2/xml/XmlPullParser.cpp
index 8abc26d..1527d68 100644
--- a/tools/aapt2/xml/XmlPullParser.cpp
+++ b/tools/aapt2/xml/XmlPullParser.cpp
@@ -309,7 +309,14 @@
 }
 
 std::optional<StringPiece> FindAttribute(const XmlPullParser* parser, StringPiece name) {
-  auto iter = parser->FindAttribute("", name);
+  return FindAttribute(parser, "", name);
+}
+
+std::optional<android::StringPiece> FindAttribute(const XmlPullParser* parser,
+                                                  android::StringPiece namespace_uri,
+                                                  android::StringPiece name) {
+  auto iter = parser->FindAttribute(namespace_uri, name);
+
   if (iter != parser->end_attributes()) {
     return StringPiece(util::TrimWhitespace(iter->value));
   }
diff --git a/tools/aapt2/xml/XmlPullParser.h b/tools/aapt2/xml/XmlPullParser.h
index 64274d0..d65ba6f 100644
--- a/tools/aapt2/xml/XmlPullParser.h
+++ b/tools/aapt2/xml/XmlPullParser.h
@@ -194,6 +194,13 @@
                                                   android::StringPiece name);
 
 /**
+ * Finds the attribute in the current element within the given namespace.
+ */
+std::optional<android::StringPiece> FindAttribute(const XmlPullParser* parser,
+                                                  android::StringPiece namespace_uri,
+                                                  android::StringPiece name);
+
+/**
  * Finds the attribute in the current element within the global namespace. The
  * attribute's value
  * must not be the empty string.