Add staging-public-group to aapt2

staging-public-group is a tag for putting resources that have been
added during platform development, but have not yet been finalized,
into a separate resource id namespace.

R.java fields of staged resources are non-final, so when the SDK is
finalized, applications using the android R.java will automatically
use the new finalized resource id without having to recompile.

Staged resources can exist either in the same type id as the type's
non-staged counterpart or in a separate type id. Multiple
staging-public-group tags each with a different type id can exist
simultaneously, which allows for multiple versions of the platform
to be developed at once.

Bug: 183411093
Test: aapt2_tests

Change-Id: Ibb6c84c3626751e33c6097f35a03e306bb85616a
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index a447cef..24c60b7 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -42,6 +42,10 @@
 using android::idmap2::policy::kPolicyStringToFlag;
 
 namespace aapt {
+namespace {
+constexpr const char* kPublicGroupTag = "public-group";
+constexpr const char* kStagingPublicGroupTag = "staging-public-group";
+}  // namespace
 
 constexpr const char* sXliffNamespaceUri = "urn:oasis:names:tc:xliff:document:1.2";
 
@@ -102,6 +106,7 @@
 
   ResourceId id;
   Visibility::Level visibility_level = Visibility::Level::kUndefined;
+  bool staged_api = false;
   bool allow_new = false;
   Maybe<OverlayableItem> overlayable_item;
 
@@ -122,6 +127,7 @@
   if (res->visibility_level != Visibility::Level::kUndefined) {
     Visibility visibility;
     visibility.level = res->visibility_level;
+    visibility.staged_api = res->staged_api;
     visibility.source = res->source;
     visibility.comment = res->comment;
     res_builder.SetVisibility(visibility);
@@ -525,6 +531,7 @@
       {"plurals", std::mem_fn(&ResourceParser::ParsePlural)},
       {"public", std::mem_fn(&ResourceParser::ParsePublic)},
       {"public-group", std::mem_fn(&ResourceParser::ParsePublicGroup)},
+      {"staging-public-group", std::mem_fn(&ResourceParser::ParseStagingPublicGroup)},
       {"string-array", std::mem_fn(&ResourceParser::ParseStringArray)},
       {"style", std::bind(&ResourceParser::ParseStyle, std::placeholders::_1, ResourceType::kStyle,
                           std::placeholders::_2, std::placeholders::_3)},
@@ -653,7 +660,8 @@
     const auto bag_iter = elToBagMap.find(resource_type);
     if (bag_iter != elToBagMap.end()) {
       // Ensure we have a name (unless this is a <public-group> or <overlayable>).
-      if (resource_type != "public-group" && resource_type != "overlayable") {
+      if (resource_type != kPublicGroupTag && resource_type != kStagingPublicGroupTag &&
+          resource_type != "overlayable") {
         if (!maybe_name) {
           diag_->Error(DiagMessage(out_resource->source)
                        << "<" << parser->element_name() << "> missing 'name' attribute");
@@ -890,54 +898,45 @@
   return true;
 }
 
-bool ResourceParser::ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource) {
-  if (options_.visibility) {
-    diag_->Error(DiagMessage(out_resource->source)
-                 << "<public-group> tag not allowed with --visibility flag");
-    return false;
-  }
-
+template <typename Func>
+bool static ParseGroupImpl(xml::XmlPullParser* parser, ParsedResource* out_resource,
+                           const char* tag_name, IDiagnostics* diag, Func&& func) {
   if (out_resource->config != ConfigDescription::DefaultConfig()) {
-    diag_->Warn(DiagMessage(out_resource->source)
-                << "ignoring configuration '" << out_resource->config
-                << "' for <public-group> tag");
+    diag->Warn(DiagMessage(out_resource->source)
+               << "ignoring configuration '" << out_resource->config << "' for <" << tag_name
+               << "> tag");
   }
 
   Maybe<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type");
   if (!maybe_type) {
-    diag_->Error(DiagMessage(out_resource->source)
-                 << "<public-group> must have a 'type' attribute");
+    diag->Error(DiagMessage(out_resource->source)
+                << "<" << tag_name << "> must have a 'type' attribute");
     return false;
   }
 
   const ResourceType* parsed_type = ParseResourceType(maybe_type.value());
   if (!parsed_type) {
-    diag_->Error(DiagMessage(out_resource->source) << "invalid resource type '"
-                                                   << maybe_type.value()
-                                                   << "' in <public-group>");
+    diag->Error(DiagMessage(out_resource->source)
+                << "invalid resource type '" << maybe_type.value() << "' in <" << tag_name << ">");
     return false;
   }
 
-  Maybe<StringPiece> maybe_id_str =
-      xml::FindNonEmptyAttribute(parser, "first-id");
+  Maybe<StringPiece> maybe_id_str = xml::FindNonEmptyAttribute(parser, "first-id");
   if (!maybe_id_str) {
-    diag_->Error(DiagMessage(out_resource->source)
-                 << "<public-group> must have a 'first-id' attribute");
+    diag->Error(DiagMessage(out_resource->source)
+                << "<" << tag_name << "> must have a 'first-id' attribute");
     return false;
   }
 
-  Maybe<ResourceId> maybe_id =
-      ResourceUtils::ParseResourceId(maybe_id_str.value());
+  Maybe<ResourceId> maybe_id = ResourceUtils::ParseResourceId(maybe_id_str.value());
   if (!maybe_id) {
-    diag_->Error(DiagMessage(out_resource->source) << "invalid resource ID '"
-                                                   << maybe_id_str.value()
-                                                   << "' in <public-group>");
+    diag->Error(DiagMessage(out_resource->source)
+                << "invalid resource ID '" << maybe_id_str.value() << "' in <" << tag_name << ">");
     return false;
   }
 
-  ResourceId next_id = maybe_id.value();
-
   std::string comment;
+  ResourceId next_id = maybe_id.value();
   bool error = false;
   const size_t depth = parser->depth();
   while (xml::XmlPullParser::NextChildNode(parser, depth)) {
@@ -949,53 +948,72 @@
       continue;
     }
 
-    const Source item_source = source_.WithLine(parser->line_number());
+    const Source item_source = out_resource->source.WithLine(parser->line_number());
     const std::string& element_namespace = parser->element_namespace();
     const std::string& element_name = parser->element_name();
     if (element_namespace.empty() && element_name == "public") {
-      Maybe<StringPiece> maybe_name =
-          xml::FindNonEmptyAttribute(parser, "name");
+      auto maybe_name = xml::FindNonEmptyAttribute(parser, "name");
       if (!maybe_name) {
-        diag_->Error(DiagMessage(item_source)
-                     << "<public> must have a 'name' attribute");
+        diag->Error(DiagMessage(item_source) << "<public> must have a 'name' attribute");
         error = true;
         continue;
       }
 
       if (xml::FindNonEmptyAttribute(parser, "id")) {
-        diag_->Error(DiagMessage(item_source)
-                     << "'id' is ignored within <public-group>");
+        diag->Error(DiagMessage(item_source) << "'id' is ignored within <" << tag_name << ">");
         error = true;
         continue;
       }
 
       if (xml::FindNonEmptyAttribute(parser, "type")) {
-        diag_->Error(DiagMessage(item_source)
-                     << "'type' is ignored within <public-group>");
+        diag->Error(DiagMessage(item_source) << "'type' is ignored within <" << tag_name << ">");
         error = true;
         continue;
       }
 
-      ParsedResource child_resource;
-      child_resource.name.type = *parsed_type;
-      child_resource.name.entry = maybe_name.value().to_string();
-      child_resource.id = next_id;
-      // NOLINTNEXTLINE(bugprone-use-after-move) move+reset comment
-      child_resource.comment = std::move(comment);
-      child_resource.source = item_source;
-      child_resource.visibility_level = Visibility::Level::kPublic;
-      out_resource->child_resources.push_back(std::move(child_resource));
+      ParsedResource& entry_res = out_resource->child_resources.emplace_back(ParsedResource{
+          .name = ResourceName{{}, *parsed_type, maybe_name.value().to_string()},
+          .source = item_source,
+          .id = next_id,
+          .comment = std::move(comment),
+      });
 
-      next_id.id += 1;
+      // Execute group specific code.
+      func(entry_res, next_id);
 
+      next_id.id++;
     } else if (!ShouldIgnoreElement(element_namespace, element_name)) {
-      diag_->Error(DiagMessage(item_source) << ":" << element_name << ">");
+      diag->Error(DiagMessage(item_source) << ":" << element_name << ">");
       error = true;
     }
   }
   return !error;
 }
 
+bool ResourceParser::ParseStagingPublicGroup(xml::XmlPullParser* parser,
+                                             ParsedResource* out_resource) {
+  return ParseGroupImpl(parser, out_resource, kStagingPublicGroupTag, diag_,
+                        [](ParsedResource& parsed_entry, ResourceId id) {
+                          parsed_entry.id = id;
+                          parsed_entry.staged_api = true;
+                          parsed_entry.visibility_level = Visibility::Level::kPublic;
+                        });
+}
+
+bool ResourceParser::ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource) {
+  if (options_.visibility) {
+    diag_->Error(DiagMessage(out_resource->source)
+                 << "<" << kPublicGroupTag << "> tag not allowed with --visibility flag");
+    return false;
+  }
+
+  return ParseGroupImpl(parser, out_resource, kPublicGroupTag, diag_,
+                        [](ParsedResource& parsed_entry, ResourceId id) {
+                          parsed_entry.id = id;
+                          parsed_entry.visibility_level = Visibility::Level::kPublic;
+                        });
+}
+
 bool ResourceParser::ParseSymbolImpl(xml::XmlPullParser* parser,
                                      ParsedResource* out_resource) {
   Maybe<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type");
diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h
index 9d3ecc8..af0db8c 100644
--- a/tools/aapt2/ResourceParser.h
+++ b/tools/aapt2/ResourceParser.h
@@ -99,6 +99,7 @@
 
   bool ParsePublic(xml::XmlPullParser* parser, ParsedResource* out_resource);
   bool ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource);
+  bool ParseStagingPublicGroup(xml::XmlPullParser* parser, ParsedResource* out_resource);
   bool ParseSymbolImpl(xml::XmlPullParser* parser, ParsedResource* out_resource);
   bool ParseSymbol(xml::XmlPullParser* parser, ParsedResource* out_resource);
   bool ParseOverlayable(xml::XmlPullParser* parser, ParsedResource* out_resource);
diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp
index 53bfa0b..4a509be 100644
--- a/tools/aapt2/ResourceParser_test.cpp
+++ b/tools/aapt2/ResourceParser_test.cpp
@@ -840,6 +840,31 @@
   EXPECT_THAT(result.value().entry->id.value(), Eq(ResourceId(0x01010041)));
 }
 
+TEST_F(ResourceParserTest, StagingPublicGroup) {
+  std::string input = R"(
+      <staging-public-group type="attr" first-id="0x01ff0049">
+        <public name="foo" />
+        <public name="bar" />
+      </staging-public-group>)";
+  ASSERT_TRUE(TestParse(input));
+
+  Maybe<ResourceTable::SearchResult> result = table_.FindResource(test::ParseNameOrDie("attr/foo"));
+  ASSERT_TRUE(result);
+
+  ASSERT_TRUE(result.value().entry->id);
+  EXPECT_THAT(result.value().entry->id.value(), Eq(ResourceId(0x01ff0049)));
+  EXPECT_THAT(result.value().entry->visibility.level, Eq(Visibility::Level::kPublic));
+  EXPECT_TRUE(result.value().entry->visibility.staged_api);
+
+  result = table_.FindResource(test::ParseNameOrDie("attr/bar"));
+  ASSERT_TRUE(result);
+
+  ASSERT_TRUE(result.value().entry->id);
+  EXPECT_THAT(result.value().entry->id.value(), Eq(ResourceId(0x01ff004a)));
+  EXPECT_THAT(result.value().entry->visibility.level, Eq(Visibility::Level::kPublic));
+  EXPECT_TRUE(result.value().entry->visibility.staged_api);
+}
+
 TEST_F(ResourceParserTest, StrongestSymbolVisibilityWins) {
   std::string input = R"(
       <!-- private -->
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index cff9872..27f7bdd 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -47,11 +47,6 @@
 }
 
 template <typename T>
-bool less_than_type_and_id(const T& lhs, const std::pair<ResourceType, Maybe<uint8_t>>& rhs) {
-  return lhs.id != rhs.second ? lhs.id < rhs.second : lhs.type < rhs.first;
-}
-
-template <typename T>
 bool less_than_struct_with_name(const std::unique_ptr<T>& lhs, const StringPiece& rhs) {
   return lhs->name.compare(0, lhs->name.size(), rhs.data(), rhs.size()) < 0;
 }
@@ -80,12 +75,6 @@
   return lhs.name.compare(0, lhs.name.size(), rhs.first.data(), rhs.first.size()) < 0;
 }
 
-template <typename T, typename U>
-bool less_than_struct_with_name_and_id_pointer(const T* lhs,
-                                               const std::pair<std::string_view, Maybe<U>>& rhs) {
-  return less_than_struct_with_name_and_id(*lhs, rhs);
-}
-
 template <typename T, typename Func, typename Elements>
 T* FindElementsRunAction(const android::StringPiece& name, Elements& entries, Func action) {
   const auto iter =
@@ -307,51 +296,115 @@
   return CollisionResult::kConflict;
 }
 
+template <typename T, typename Comparer>
+struct SortedVectorInserter : public Comparer {
+  std::pair<bool, typename std::vector<T>::iterator> LowerBound(std::vector<T>& el,
+                                                                const T& value) {
+    auto it = std::lower_bound(el.begin(), el.end(), value, [&](auto& lhs, auto& rhs) {
+      return Comparer::operator()(lhs, rhs);
+    });
+    bool found =
+        it != el.end() && !Comparer::operator()(*it, value) && !Comparer::operator()(value, *it);
+    return std::make_pair(found, it);
+  }
+
+  T* Insert(std::vector<T>& el, T&& value) {
+    auto [found, it] = LowerBound(el, value);
+    if (found) {
+      return &*it;
+    }
+    return &*el.insert(it, std::move(value));
+  }
+};
+
+struct PackageViewComparer {
+  bool operator()(const ResourceTablePackageView& lhs, const ResourceTablePackageView& rhs) {
+    return less_than_struct_with_name_and_id<ResourceTablePackageView, uint8_t>(
+        lhs, std::make_pair(rhs.name, rhs.id));
+  }
+};
+
+struct TypeViewComparer {
+  bool operator()(const ResourceTableTypeView& lhs, const ResourceTableTypeView& rhs) {
+    return lhs.id != rhs.id ? lhs.id < rhs.id : lhs.type < rhs.type;
+  }
+};
+
+struct EntryViewComparer {
+  bool operator()(const ResourceEntry* lhs, const ResourceEntry* rhs) {
+    return less_than_struct_with_name_and_id<ResourceEntry, ResourceId>(
+        *lhs, std::make_pair(rhs->name, rhs->id));
+  }
+};
+
 ResourceTableView ResourceTable::GetPartitionedView() const {
   ResourceTableView view;
+  SortedVectorInserter<ResourceTablePackageView, PackageViewComparer> package_inserter;
+  SortedVectorInserter<ResourceTableTypeView, TypeViewComparer> type_inserter;
+  SortedVectorInserter<const ResourceEntry*, EntryViewComparer> entry_inserter;
+
   for (const auto& package : packages) {
     for (const auto& type : package->types) {
       for (const auto& entry : type->entries) {
-        std::pair<std::string_view, Maybe<uint8_t>> package_key(package->name, {});
-        std::pair<std::string_view, Maybe<ResourceId>> entry_key(entry->name, {});
-        std::pair<ResourceType, Maybe<uint8_t>> type_key(type->type, {});
-        if (entry->id) {
-          // If the entry has a defined id, use the id to determine insertion position.
-          package_key.second = entry->id.value().package_id();
-          type_key.second = entry->id.value().type_id();
-          entry_key.second = entry->id.value();
-        }
+        ResourceTablePackageView new_package{
+            package->name, entry->id ? entry->id.value().package_id() : Maybe<uint8_t>{}};
+        auto view_package = package_inserter.Insert(view.packages, std::move(new_package));
 
-        auto package_it =
-            std::lower_bound(view.packages.begin(), view.packages.end(), package_key,
-                             less_than_struct_with_name_and_id<ResourceTablePackageView, uint8_t>);
-        if (package_it == view.packages.end() || package_it->name != package_key.first ||
-            package_it->id != package_key.second) {
-          ResourceTablePackageView new_package{std::string(package_key.first), package_key.second};
-          package_it = view.packages.insert(package_it, new_package);
-        }
-
-        auto type_it = std::lower_bound(package_it->types.begin(), package_it->types.end(),
-                                        type_key, less_than_type_and_id<ResourceTableTypeView>);
-        if (type_it == package_it->types.end() || type_key.first != type_it->type ||
-            type_it->id != type_key.second) {
-          ResourceTableTypeView new_type{type_key.first, type_key.second};
-          type_it = package_it->types.insert(type_it, new_type);
-        }
+        ResourceTableTypeView new_type{type->type,
+                                       entry->id ? entry->id.value().type_id() : Maybe<uint8_t>{}};
+        auto view_type = type_inserter.Insert(view_package->types, std::move(new_type));
 
         if (entry->visibility.level == Visibility::Level::kPublic) {
           // Only mark the type visibility level as public, it doesn't care about being private.
-          type_it->visibility_level = Visibility::Level::kPublic;
+          view_type->visibility_level = Visibility::Level::kPublic;
         }
 
-        auto entry_it =
-            std::lower_bound(type_it->entries.begin(), type_it->entries.end(), entry_key,
-                             less_than_struct_with_name_and_id_pointer<ResourceEntry, ResourceId>);
-        type_it->entries.insert(entry_it, entry.get());
+        entry_inserter.Insert(view_type->entries, entry.get());
       }
     }
   }
 
+  // The android runtime does not support querying resources when the there are multiple type ids
+  // for the same resource type within the same package. For this reason, if there are types with
+  // multiple type ids, each type needs to exist in its own package in order to be queried by name.
+  std::vector<ResourceTablePackageView> new_packages;
+  for (auto& package : view.packages) {
+    // If a new package was already created for a different type within this package, then
+    // we can reuse those packages for other types that need to be extracted from this package.
+    // `start_index` is the index of the first newly created package that can be reused.
+    const size_t start_index = new_packages.size();
+    std::map<ResourceType, size_t> type_new_package_index;
+    for (auto type_it = package.types.begin(); type_it != package.types.end();) {
+      auto& type = *type_it;
+      auto type_index_iter = type_new_package_index.find(type.type);
+      if (type_index_iter == type_new_package_index.end()) {
+        // First occurrence of the resource type in this package. Keep it in this package.
+        type_new_package_index.insert(type_index_iter, std::make_pair(type.type, start_index));
+        ++type_it;
+        continue;
+      }
+
+      // The resource type has already been seen for this package, so this type must be extracted to
+      // a new separate package.
+      const size_t index = type_index_iter->second;
+      if (new_packages.size() == index) {
+        new_packages.emplace_back(ResourceTablePackageView{package.name, package.id});
+        type_new_package_index[type.type] = index + 1;
+      }
+
+      // Move the type into a new package
+      auto& other_package = new_packages[index];
+      type_inserter.Insert(other_package.types, std::move(type));
+      type_it = package.types.erase(type_it);
+    }
+  }
+
+  for (auto& new_package : new_packages) {
+    // Insert newly created packages after their original packages
+    auto [_, it] = package_inserter.LowerBound(view.packages, new_package);
+    view.packages.insert(++it, std::move(new_package));
+  }
+
   return view;
 }
 
@@ -424,6 +477,10 @@
       // This symbol definition takes precedence, replace.
       entry->visibility = res.visibility.value();
     }
+
+    if (res.visibility->staged_api) {
+      entry->visibility.staged_api = entry->visibility.staged_api;
+    }
   }
 
   if (res.overlayable.has_value()) {
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 49392a5..080ecc2 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -51,6 +51,11 @@
   Level level = Level::kUndefined;
   Source source;
   std::string comment;
+
+  // Indicates that the resource id may change across builds and that the public R.java identifier
+  // for this resource should not be final. This is set to `true` for resources in `staging-group`
+  // tags.
+  bool staged_api = false;
 };
 
 // Represents <add-resource> in an overlay.
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index b1e1a77..4247ec5 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -132,6 +132,11 @@
 
   // The comment associated with the <public> tag.
   string comment = 3;
+
+  // Indicates that the resource id may change across builds and that the public R.java identifier
+  // for this resource should not be final. This is set to `true` for resources in `staging-group`
+  // tags.
+  bool staged_api = 4;
 }
 
 // Whether a resource comes from a compile-time overlay and is explicitly allowed to not overlay an
diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp
index 27cbe88..dfdac6b 100644
--- a/tools/aapt2/cmd/Link_test.cpp
+++ b/tools/aapt2/cmd/Link_test.cpp
@@ -25,6 +25,7 @@
 using testing::Eq;
 using testing::HasSubstr;
 using testing::Ne;
+using testing::NotNull;
 
 namespace aapt {
 
@@ -400,4 +401,127 @@
   EXPECT_THAT(client_r_contents, HasSubstr(" com.example.lib.R.attr.foo, 0x7f010000"));
 }
 
+TEST_F(LinkTest, StagedAndroidApi) {
+  StdErrDiagnostics diag;
+  const std::string android_values =
+      R"(<resources>
+          <public type="attr" name="finalized_res" id="0x01010001"/>
+
+          <!-- S staged attributes (support staged resources in the same type id) -->
+          <staging-public-group type="attr" first-id="0x01010050">
+            <public name="staged_s_res" />
+          </staging-public-group>
+
+          <!-- SV2 staged attributes (support staged resources in a separate type id) -->
+          <staging-public-group type="attr" first-id="0x01ff0049">
+            <public name="staged_s2_res" />
+          </staging-public-group>
+
+          <!-- T staged attributes (support staged resources in multiple separate type ids) -->
+          <staging-public-group type="attr" first-id="0x01fe0063">
+            <public name="staged_t_res" />
+          </staging-public-group>
+
+          <staging-public-group type="string" first-id="0x01fd0072">
+            <public name="staged_t_string" />
+          </staging-public-group>
+
+          <attr name="finalized_res" />
+          <attr name="staged_s_res" />
+          <attr name="staged_s2_res" />
+          <attr name="staged_t_res" />
+          <string name="staged_t_string">Hello</string>
+         </resources>)";
+
+  const std::string app_values =
+      R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+           <attr name="bar" />
+           <declare-styleable name="ClientStyleable">
+             <attr name="android:finalized_res" />
+             <attr name="android:staged_s_res" />
+             <attr name="bar" />
+           </declare-styleable>
+         </resources>)";
+
+  const std::string android_res = GetTestPath("android-res");
+  ASSERT_TRUE(
+      CompileFile(GetTestPath("res/values/values.xml"), android_values, android_res, &diag));
+
+  const std::string android_apk = GetTestPath("android.apk");
+  const std::string android_java = GetTestPath("android_java");
+  // clang-format off
+  auto android_manifest = ManifestBuilder(this)
+      .SetPackageName("android")
+      .Build();
+
+  auto android_link_args = LinkCommandBuilder(this)
+      .SetManifestFile(android_manifest)
+      .AddParameter("--private-symbols", "com.android.internal")
+      .AddParameter("--java", android_java)
+      .AddCompiledResDir(android_res, &diag)
+      .Build(android_apk);
+  // clang-format on
+  ASSERT_TRUE(Link(android_link_args, &diag));
+
+  const std::string android_r_java = android_java + "/android/R.java";
+  std::string android_r_contents;
+  ASSERT_TRUE(android::base::ReadFileToString(android_r_java, &android_r_contents));
+  EXPECT_THAT(android_r_contents, HasSubstr(" public static final int finalized_res=0x01010001;"));
+  EXPECT_THAT(android_r_contents, HasSubstr(" public static int staged_s_res=0x01010050;"));
+  EXPECT_THAT(android_r_contents, HasSubstr(" public static int staged_s2_res=0x01ff0049;"));
+  EXPECT_THAT(android_r_contents, HasSubstr(" public static int staged_t_res=0x01fe0063;"));
+  EXPECT_THAT(android_r_contents, HasSubstr(" public static int staged_t_string=0x01fd0072;"));
+
+  // Build an app that uses the framework attribute in a declare-styleable
+  const std::string client_res = GetTestPath("app-res");
+  ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"), app_values, client_res, &diag));
+
+  const std::string app_apk = GetTestPath("app.apk");
+  const std::string app_java = GetTestPath("app_java");
+  // clang-format off
+  auto app_manifest = ManifestBuilder(this)
+      .SetPackageName("com.example.app")
+      .Build();
+
+  auto app_link_args = LinkCommandBuilder(this)
+      .SetManifestFile(app_manifest)
+      .AddParameter("--java", app_java)
+      .AddParameter("-I", android_apk)
+      .AddCompiledResDir(client_res, &diag)
+      .Build(app_apk);
+  // clang-format on
+  ASSERT_TRUE(Link(app_link_args, &diag));
+
+  const std::string client_r_java = app_java + "/com/example/app/R.java";
+  std::string client_r_contents;
+  ASSERT_TRUE(android::base::ReadFileToString(client_r_java, &client_r_contents));
+  EXPECT_THAT(client_r_contents, HasSubstr(" 0x01010001, android.R.attr.staged_s_res, 0x7f010000"));
+
+  // Test that the resource ids of staged and non-staged resource can be retrieved
+  android::AssetManager2 am;
+  auto android_asset = android::ApkAssets::Load(android_apk);
+  ASSERT_THAT(android_asset, NotNull());
+  ASSERT_TRUE(am.SetApkAssets({android_asset.get()}));
+
+  auto result = am.GetResourceId("android:attr/finalized_res");
+  ASSERT_TRUE(result.has_value());
+  EXPECT_THAT(*result, Eq(0x01010001));
+
+  result = am.GetResourceId("android:attr/staged_s_res");
+  ASSERT_TRUE(result.has_value());
+  EXPECT_THAT(*result, Eq(0x01010050));
+
+  result = am.GetResourceId("android:attr/staged_s2_res");
+  ASSERT_TRUE(result.has_value());
+  EXPECT_THAT(*result, Eq(0x01ff0049));
+
+  result = am.GetResourceId("android:attr/staged_t_res");
+  ASSERT_TRUE(result.has_value());
+  EXPECT_THAT(*result, Eq(0x01fe0063));
+
+  result = am.GetResourceId("android:string/staged_t_string");
+  ASSERT_TRUE(result.has_value());
+  EXPECT_THAT(*result, Eq(0x01fd0072));
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/compile/IdAssigner.cpp b/tools/aapt2/compile/IdAssigner.cpp
index 07db73d..9a50b26 100644
--- a/tools/aapt2/compile/IdAssigner.cpp
+++ b/tools/aapt2/compile/IdAssigner.cpp
@@ -77,6 +77,27 @@
   NextIdFinder<uint16_t, ResourceName> next_entry_id_;
 };
 
+struct ResourceTypeKey {
+  ResourceType type;
+  uint8_t id;
+
+  bool operator<(const ResourceTypeKey& other) const {
+    return (type != other.type) ? type < other.type : id < other.id;
+  }
+
+  bool operator==(const ResourceTypeKey& other) const {
+    return type == other.type && id == other.id;
+  }
+
+  bool operator!=(const ResourceTypeKey& other) const {
+    return !(*this == other);
+  }
+};
+
+::std::ostream& operator<<(::std::ostream& out, const ResourceTypeKey& type) {
+  return out << type.type;
+}
+
 struct IdAssignerContext {
   IdAssignerContext(std::string package_name, uint8_t package_id)
       : package_name_(std::move(package_name)), package_id_(package_id) {
@@ -85,7 +106,8 @@
   // Attempts to reserve the resource id for the specified resource name.
   // Returns whether the id was reserved successfully.
   // Reserving identifiers must be completed before `NextId` is called for the first time.
-  bool ReserveId(const ResourceName& name, ResourceId id, IDiagnostics* diag);
+  bool ReserveId(const ResourceName& name, ResourceId id, const Visibility& visibility,
+                 IDiagnostics* diag);
 
   // Retrieves the next available resource id that has not been reserved.
   std::optional<ResourceId> NextId(const ResourceName& name, IDiagnostics* diag);
@@ -93,8 +115,10 @@
  private:
   std::string package_name_;
   uint8_t package_id_;
-  std::map<ResourceType, TypeGroup> types_;
-  NextIdFinder<uint8_t, ResourceType> type_id_finder_ = NextIdFinder<uint8_t, ResourceType>(1);
+  std::map<ResourceTypeKey, TypeGroup> types_;
+  std::map<ResourceType, uint8_t> non_staged_type_ids_;
+  NextIdFinder<uint8_t, ResourceTypeKey> type_id_finder_ =
+      NextIdFinder<uint8_t, ResourceTypeKey>(1);
 };
 
 }  // namespace
@@ -106,7 +130,8 @@
       for (auto& entry : type->entries) {
         const ResourceName name(package->name, type->type, entry->name);
         if (entry->id) {
-          if (!assigned_ids.ReserveId(name, entry->id.value(), context->GetDiagnostics())) {
+          if (!assigned_ids.ReserveId(name, entry->id.value(), entry->visibility,
+                                      context->GetDiagnostics())) {
             return false;
           }
         }
@@ -116,7 +141,8 @@
           const auto iter = assigned_id_map_->find(name);
           if (iter != assigned_id_map_->end()) {
             const ResourceId assigned_id = iter->second;
-            if (!assigned_ids.ReserveId(name, assigned_id, context->GetDiagnostics())) {
+            if (!assigned_ids.ReserveId(name, assigned_id, entry->visibility,
+                                        context->GetDiagnostics())) {
               return false;
             }
             entry->id = assigned_id;
@@ -132,7 +158,8 @@
     for (const auto& stable_id_entry : *assigned_id_map_) {
       const ResourceName& pre_assigned_name = stable_id_entry.first;
       const ResourceId& pre_assigned_id = stable_id_entry.second;
-      if (!assigned_ids.ReserveId(pre_assigned_name, pre_assigned_id, context->GetDiagnostics())) {
+      if (!assigned_ids.ReserveId(pre_assigned_name, pre_assigned_id, {},
+                                  context->GetDiagnostics())) {
         return false;
       }
     }
@@ -165,7 +192,7 @@
   auto assign_result = pre_assigned_ids_.emplace(id, key);
   if (!assign_result.second && assign_result.first->second != key) {
     std::stringstream error;
-    error << "ID " << id << " is already assigned to " << assign_result.first->second;
+    error << "ID is already assigned to " << assign_result.first->second;
     return unexpected(error.str());
   }
   return id;
@@ -210,7 +237,7 @@
   if (type_id_ != id.type_id()) {
     // Currently there cannot be multiple type ids for a single type.
     std::stringstream error;
-    error << "type '" << name.type << "' already has ID " << id.type_id();
+    error << "type '" << name.type << "' already has ID " << std::hex << (int)id.type_id();
     return unexpected(error.str());
   }
 
@@ -234,24 +261,38 @@
   return ResourceId(package_id_, type_id_, entry_id.value());
 }
 
-bool IdAssignerContext::ReserveId(const ResourceName& name, ResourceId id, IDiagnostics* diag) {
+bool IdAssignerContext::ReserveId(const ResourceName& name, ResourceId id,
+                                  const Visibility& visibility, IDiagnostics* diag) {
   if (package_id_ != id.package_id()) {
     diag->Error(DiagMessage() << "can't assign ID " << id << " to resource " << name
-                              << " because package already has ID " << id.package_id());
+                              << " because package already has ID " << std::hex
+                              << (int)id.package_id());
     return false;
   }
 
-  auto type = types_.find(name.type);
+  auto key = ResourceTypeKey{name.type, id.type_id()};
+  auto type = types_.find(key);
   if (type == types_.end()) {
     // The type has not been assigned an id yet. Ensure that the specified id is not being used by
     // another type.
-    auto assign_result = type_id_finder_.ReserveId(name.type, id.type_id());
+    auto assign_result = type_id_finder_.ReserveId(key, id.type_id());
     if (!assign_result.has_value()) {
       diag->Error(DiagMessage() << "can't assign ID " << id << " to resource " << name
                                 << " because type " << assign_result.error());
       return false;
     }
-    type = types_.emplace(name.type, TypeGroup(package_id_, id.type_id())).first;
+    type = types_.emplace(key, TypeGroup(package_id_, id.type_id())).first;
+  }
+
+  if (!visibility.staged_api) {
+    // Ensure that non-staged resources can only exist in one type ID.
+    auto non_staged_type = non_staged_type_ids_.emplace(name.type, id.type_id());
+    if (!non_staged_type.second && non_staged_type.first->second != id.type_id()) {
+      diag->Error(DiagMessage() << "can't assign ID " << id << " to resource " << name
+                                << " because type already has ID " << std::hex
+                                << (int)id.type_id());
+      return false;
+    }
   }
 
   auto assign_result = type->second.ReserveId(name, id);
@@ -268,11 +309,19 @@
   // The package name is not known during the compile stage.
   // Resources without a package name are considered a part of the app being linked.
   CHECK(name.package.empty() || name.package == package_name_);
-  auto type = types_.find(name.type);
-  if (type == types_.end()) {
+
+  // Find the type id for non-staged resources of this type.
+  auto non_staged_type = non_staged_type_ids_.find(name.type);
+  if (non_staged_type == non_staged_type_ids_.end()) {
     auto next_type_id = type_id_finder_.NextId();
     CHECK(next_type_id.has_value()) << "resource type IDs allocated have exceeded maximum (256)";
-    type = types_.emplace(name.type, TypeGroup(package_id_, next_type_id.value())).first;
+    non_staged_type = non_staged_type_ids_.emplace(name.type, *next_type_id).first;
+  }
+
+  ResourceTypeKey key{name.type, non_staged_type->second};
+  auto type = types_.find(key);
+  if (type == types_.end()) {
+    type = types_.emplace(key, TypeGroup(package_id_, key.id)).first;
   }
 
   auto assign_result = type->second.NextId();
diff --git a/tools/aapt2/compile/IdAssigner_test.cpp b/tools/aapt2/compile/IdAssigner_test.cpp
index 0065a22..6637766 100644
--- a/tools/aapt2/compile/IdAssigner_test.cpp
+++ b/tools/aapt2/compile/IdAssigner_test.cpp
@@ -98,6 +98,37 @@
   ASSERT_FALSE(assigner.Consume(context.get(), table.get()));
 }
 
+TEST_F(IdAssignerTests, FailWhenNonUniqueTypeIdsAssigned) {
+  auto table = test::ResourceTableBuilder()
+                   .AddSimple("android:string/foo", ResourceId(0x01040000))
+                   .AddSimple("android:attr/bar", ResourceId(0x01040006))
+                   .Build();
+  IdAssigner assigner;
+  ASSERT_FALSE(assigner.Consume(context.get(), table.get()));
+}
+
+TEST_F(IdAssignerTests, FailWhenTypeHasTwoNonStagedIds) {
+  auto table = test::ResourceTableBuilder()
+                   .AddSimple("android:attr/foo", ResourceId(0x01050000))
+                   .AddSimple("android:attr/bar", ResourceId(0x01040006))
+                   .Build();
+  IdAssigner assigner;
+  ASSERT_FALSE(assigner.Consume(context.get(), table.get()));
+}
+
+TEST_F(IdAssignerTests, FailWhenTypeHasTwoNonStagedIdsRegardlessOfStagedId) {
+  auto table = test::ResourceTableBuilder()
+                   .AddSimple("android:attr/foo", ResourceId(0x01050000))
+                   .AddSimple("android:attr/bar", ResourceId(0x01ff0006))
+                   .Add(NewResourceBuilder("android:attr/staged_baz")
+                            .SetId(0x01ff0000)
+                            .SetVisibility({.staged_api = true})
+                            .Build())
+                   .Build();
+  IdAssigner assigner;
+  ASSERT_FALSE(assigner.Consume(context.get(), table.get()));
+}
+
 TEST_F(IdAssignerTests, AssignIdsWithIdMap) {
   auto table = test::ResourceTableBuilder()
                    .AddSimple("android:attr/foo")
@@ -154,52 +185,24 @@
 }
 
 ::testing::AssertionResult VerifyIds(ResourceTable* table) {
-  std::set<uint8_t> package_ids;
-  auto table_view = table->GetPartitionedView();
-  for (auto& package : table_view.packages) {
-    if (!package.id) {
-      return ::testing::AssertionFailure() << "package " << package.name << " has no ID";
-    }
-
-    if (!package_ids.insert(package.id.value()).second) {
-      return ::testing::AssertionFailure() << "package " << package.name << " has non-unique ID "
-                                           << std::hex << (int)package.id.value() << std::dec;
-    }
-  }
-
-  for (auto& package : table_view.packages) {
-    std::set<uint8_t> type_ids;
-    for (auto& type : package.types) {
-      if (!type.id) {
-        return ::testing::AssertionFailure()
-               << "type " << type.type << " of package " << package.name << " has no ID";
-      }
-
-      if (!type_ids.insert(type.id.value()).second) {
-        return ::testing::AssertionFailure()
-               << "type " << type.type << " of package " << package.name << " has non-unique ID "
-               << std::hex << (int)type.id.value() << std::dec;
-      }
-    }
-
-    for (auto& type : package.types) {
-      std::set<ResourceId> entry_ids;
-      for (auto& entry : type.entries) {
+  std::set<ResourceId> seen_ids;
+  for (auto& package : table->packages) {
+    for (auto& type : package->types) {
+      for (auto& entry : type->entries) {
         if (!entry->id) {
           return ::testing::AssertionFailure()
-                 << "entry " << entry->name << " of type " << type.type << " of package "
-                 << package.name << " has no ID";
+                 << "resource " << ResourceNameRef(package->name, type->type, entry->name)
+                 << " has no ID";
         }
-
-        if (!entry_ids.insert(entry->id.value()).second) {
+        if (!seen_ids.insert(entry->id.value()).second) {
           return ::testing::AssertionFailure()
-                 << "entry " << entry->name << " of type " << type.type << " of package "
-                 << package.name << " has non-unique ID " << std::hex << entry->id.value()
-                 << std::dec;
+                 << "resource " << ResourceNameRef(package->name, type->type, entry->name)
+                 << " has a non-unique ID" << std::hex << entry->id.value() << std::dec;
         }
       }
     }
   }
+
   return ::testing::AssertionSuccess() << "all IDs are unique and assigned";
 }
 
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 5fea897..17d11a6 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -546,8 +546,12 @@
       const uint16_t entry_id = entry->id.value().entry_id();
 
       // Populate the config masks for this entry.
+      uint32_t& entry_config_masks = config_masks[entry_id];
       if (entry->visibility.level == Visibility::Level::kPublic) {
-        config_masks[entry_id] |= util::HostToDevice32(ResTable_typeSpec::SPEC_PUBLIC);
+        entry_config_masks |= util::HostToDevice32(ResTable_typeSpec::SPEC_PUBLIC);
+      }
+      if (entry->visibility.staged_api) {
+        entry_config_masks |= util::HostToDevice32(ResTable_typeSpec::SPEC_STAGED_API);
       }
 
       const size_t config_count = entry->values.size();
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index bfb92da..498d5a2 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -456,6 +456,7 @@
           DeserializeSourceFromPb(pb_visibility.source(), src_pool, &entry->visibility.source);
         }
         entry->visibility.comment = pb_visibility.comment();
+        entry->visibility.staged_api = pb_visibility.staged_api();
 
         const Visibility::Level level = DeserializeVisibilityFromPb(pb_visibility.level());
         entry->visibility.level = level;
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 9842e25..f13f82d 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -378,6 +378,7 @@
 
         // Write the Visibility struct.
         pb::Visibility* pb_visibility = pb_entry->mutable_visibility();
+        pb_visibility->set_staged_api(entry->visibility.staged_api);
         pb_visibility->set_level(SerializeVisibilityToPb(entry->visibility.level));
         if (source_pool != nullptr) {
           SerializeSourceToPb(entry->visibility.source, source_pool.get(),
diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
index ad5ed4d..591ba149 100644
--- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
@@ -50,6 +50,53 @@
   return (result) ? result.value().entry : nullptr;
 }
 
+TEST(ProtoSerializeTest, SerializeVisibility) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .Add(NewResourceBuilder("com.app.a:bool/foo")
+                   .SetVisibility({Visibility::Level::kUndefined})
+                   .Build())
+          .Add(NewResourceBuilder("com.app.a:bool/bar")
+                   .SetVisibility({Visibility::Level::kPrivate})
+                   .Build())
+          .Add(NewResourceBuilder("com.app.a:bool/baz")
+                   .SetVisibility({Visibility::Level::kPublic})
+                   .Build())
+          .Add(NewResourceBuilder("com.app.a:bool/fiz")
+                   .SetVisibility({.level = Visibility::Level::kPublic, .staged_api = true})
+                   .Build())
+          .Build();
+
+  ResourceTable new_table;
+  pb::ResourceTable pb_table;
+  MockFileCollection files;
+  std::string error;
+  SerializeTableToPb(*table, &pb_table, context->GetDiagnostics());
+  ASSERT_TRUE(DeserializeTableFromPb(pb_table, &files, &new_table, &error));
+  EXPECT_THAT(error, IsEmpty());
+
+  auto search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/foo"));
+  ASSERT_TRUE(search_result);
+  EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined));
+  EXPECT_FALSE(search_result.value().entry->visibility.staged_api);
+
+  search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/bar"));
+  ASSERT_TRUE(search_result);
+  EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kPrivate));
+  EXPECT_FALSE(search_result.value().entry->visibility.staged_api);
+
+  search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/baz"));
+  ASSERT_TRUE(search_result);
+  EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kPublic));
+  EXPECT_FALSE(search_result.value().entry->visibility.staged_api);
+
+  search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/fiz"));
+  ASSERT_TRUE(search_result);
+  EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kPublic));
+  EXPECT_TRUE(search_result.value().entry->visibility.staged_api);
+}
+
 TEST(ProtoSerializeTest, SerializeSinglePackage) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
   std::unique_ptr<ResourceTable> table =
diff --git a/tools/aapt2/java/ClassDefinition.h b/tools/aapt2/java/ClassDefinition.h
index 995495a..d3648c8 100644
--- a/tools/aapt2/java/ClassDefinition.h
+++ b/tools/aapt2/java/ClassDefinition.h
@@ -59,8 +59,9 @@
 template <typename T>
 class PrimitiveMember : public ClassMember {
  public:
-  PrimitiveMember(const android::StringPiece& name, const T& val)
-      : name_(name.to_string()), val_(val) {}
+  PrimitiveMember(const android::StringPiece& name, const T& val, bool staged_api = false)
+      : name_(name.to_string()), val_(val), staged_api_(staged_api) {
+  }
 
   bool empty() const override {
     return false;
@@ -77,7 +78,7 @@
     ClassMember::Print(final, printer, strip_api_annotations);
 
     printer->Print("public static ");
-    if (final) {
+    if (final && !staged_api_) {
       printer->Print("final ");
     }
     printer->Print("int ").Print(name_).Print("=").Print(to_string(val_)).Print(";");
@@ -88,14 +89,16 @@
 
   std::string name_;
   T val_;
+  bool staged_api_;
 };
 
 // Specialization for strings so they get the right type and are quoted with "".
 template <>
 class PrimitiveMember<std::string> : public ClassMember {
  public:
-  PrimitiveMember(const android::StringPiece& name, const std::string& val)
-      : name_(name.to_string()), val_(val) {}
+  PrimitiveMember(const android::StringPiece& name, const std::string& val, bool staged_api = false)
+      : name_(name.to_string()), val_(val) {
+  }
 
   bool empty() const override {
     return false;
diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp
index 039448e..e1e2e01 100644
--- a/tools/aapt2/java/JavaClassGenerator.cpp
+++ b/tools/aapt2/java/JavaClassGenerator.cpp
@@ -460,7 +460,8 @@
 
   const std::string field_name = TransformToFieldName(name.entry);
   if (out_class_def != nullptr) {
-    auto resource_member = util::make_unique<ResourceMember>(field_name, real_id);
+    auto resource_member =
+        util::make_unique<ResourceMember>(field_name, real_id, entry.visibility.staged_api);
 
     // Build the comments and annotations for this entry.
     AnnotationProcessor* processor = resource_member->GetCommentBuilder();
diff --git a/tools/aapt2/process/SymbolTable.cpp b/tools/aapt2/process/SymbolTable.cpp
index de72334..d385267 100644
--- a/tools/aapt2/process/SymbolTable.cpp
+++ b/tools/aapt2/process/SymbolTable.cpp
@@ -199,7 +199,8 @@
 
   if (sr.entry->id) {
     symbol->id = sr.entry->id.value();
-    symbol->is_dynamic = (sr.entry->id.value().package_id() == 0);
+    symbol->is_dynamic =
+        (sr.entry->id.value().package_id() == 0) || sr.entry->visibility.staged_api;
   }
 
   if (name.type == ResourceType::kAttr || name.type == ResourceType::kAttrPrivate) {
@@ -374,7 +375,8 @@
 
   if (s) {
     s->is_public = (type_spec_flags & android::ResTable_typeSpec::SPEC_PUBLIC) != 0;
-    s->is_dynamic = IsPackageDynamic(ResourceId(res_id).package_id(), real_name.package);
+    s->is_dynamic = IsPackageDynamic(ResourceId(res_id).package_id(), real_name.package) ||
+                    (type_spec_flags & android::ResTable_typeSpec::SPEC_STAGED_API) != 0;
     return s;
   }
   return {};
@@ -421,7 +423,8 @@
 
   if (s) {
     s->is_public = (*flags & android::ResTable_typeSpec::SPEC_PUBLIC) != 0;
-    s->is_dynamic = IsPackageDynamic(ResourceId(id).package_id(), name.package);
+    s->is_dynamic = IsPackageDynamic(ResourceId(id).package_id(), name.package) ||
+                    (*flags & android::ResTable_typeSpec::SPEC_STAGED_API) != 0;
     return s;
   }
   return {};