diff options
-rw-r--r-- | libs/androidfw/include/androidfw/ResourceTypes.h | 5 | ||||
-rw-r--r-- | tools/aapt2/ResourceParser.cpp | 112 | ||||
-rw-r--r-- | tools/aapt2/ResourceParser.h | 1 | ||||
-rw-r--r-- | tools/aapt2/ResourceParser_test.cpp | 25 | ||||
-rw-r--r-- | tools/aapt2/ResourceTable.cpp | 139 | ||||
-rw-r--r-- | tools/aapt2/ResourceTable.h | 5 | ||||
-rw-r--r-- | tools/aapt2/Resources.proto | 5 | ||||
-rw-r--r-- | tools/aapt2/cmd/Link_test.cpp | 124 | ||||
-rw-r--r-- | tools/aapt2/compile/IdAssigner.cpp | 81 | ||||
-rw-r--r-- | tools/aapt2/compile/IdAssigner_test.cpp | 79 | ||||
-rw-r--r-- | tools/aapt2/format/binary/TableFlattener.cpp | 6 | ||||
-rw-r--r-- | tools/aapt2/format/proto/ProtoDeserialize.cpp | 1 | ||||
-rw-r--r-- | tools/aapt2/format/proto/ProtoSerialize.cpp | 1 | ||||
-rw-r--r-- | tools/aapt2/format/proto/ProtoSerialize_test.cpp | 47 | ||||
-rw-r--r-- | tools/aapt2/java/ClassDefinition.h | 13 | ||||
-rw-r--r-- | tools/aapt2/java/JavaClassGenerator.cpp | 3 | ||||
-rw-r--r-- | tools/aapt2/process/SymbolTable.cpp | 9 |
17 files changed, 504 insertions, 152 deletions
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 168a863df2bc..1e90b7c71376 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1379,6 +1379,11 @@ struct ResTable_typeSpec enum : uint32_t { // Additional flag indicating an entry is public. SPEC_PUBLIC = 0x40000000u, + + // Additional flag indicating the resource id for this resource may change in a future + // build. If this flag is set, the SPEC_PUBLIC flag is also set since the resource must be + // public to be exposed as an API to other applications. + SPEC_STAGED_API = 0x20000000u, }; }; diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index a447cef6dbbb..24c60b740bc3 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -42,6 +42,10 @@ using ::android::StringPiece; 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 @@ struct ParsedResource { 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 @@ static bool AddResourcesToTable(ResourceTable* table, IDiagnostics* diag, Parsed 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 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, {"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 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, 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 @@ bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, ParsedResource* out 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 @@ bool ResourceParser::ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource 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 9d3ecc866c5d..af0db8c0ba2c 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -99,6 +99,7 @@ class ResourceParser { 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 53bfa0b37475..4a509be56776 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -840,6 +840,31 @@ TEST_F(ResourceParserTest, AutoIncrementIdsInPublicGroup) { 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 cff98728c325..27f7bdd83c9e 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -47,11 +47,6 @@ bool less_than_type(const std::unique_ptr<ResourceTableType>& lhs, ResourceType } 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 @@ bool less_than_struct_with_name_and_id(const T& lhs, 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 @@ ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* exist 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 @@ bool ResourceTable::AddResource(NewResource&& res, IDiagnostics* diag) { // 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 49392a5db830..080ecc20558b 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -51,6 +51,11 @@ struct Visibility { 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 b1e1a77e1224..4247ec5a3495 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -132,6 +132,11 @@ message Visibility { // 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 27cbe88fde38..dfdac6b9d93e 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 @@ TEST_F(LinkTest, SharedLibraryAttributeRJava) { 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 07db73dff2c5..9a50b263c006 100644 --- a/tools/aapt2/compile/IdAssigner.cpp +++ b/tools/aapt2/compile/IdAssigner.cpp @@ -77,6 +77,27 @@ struct TypeGroup { 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 @@ struct IdAssignerContext { // 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 @@ struct IdAssignerContext { 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 @@ bool IdAssigner::Consume(IAaptContext* context, ResourceTable* table) { 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 @@ bool IdAssigner::Consume(IAaptContext* context, ResourceTable* table) { 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 @@ bool IdAssigner::Consume(IAaptContext* context, ResourceTable* table) { 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 @@ Result<Id> NextIdFinder<Id, Key>::ReserveId(Key key, Id id) { 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 @@ Result<std::monostate> TypeGroup::ReserveId(const ResourceName& name, ResourceId 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 @@ Result<ResourceId> TypeGroup::NextId() { 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 @@ std::optional<ResourceId> IdAssignerContext::NextId(const ResourceName& name, ID // 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 0065a224bfa7..663776645990 100644 --- a/tools/aapt2/compile/IdAssigner_test.cpp +++ b/tools/aapt2/compile/IdAssigner_test.cpp @@ -98,6 +98,37 @@ TEST_F(IdAssignerTests, FailWhenNonUniqueIdsAssigned) { 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 @@ TEST_F(IdAssignerTests, ExaustEntryIdsLastIdIsPublic) { } ::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 5fea897d46e7..17d11a63553a 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -546,8 +546,12 @@ class PackageFlattener { 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 bfb92da27e4d..498d5a27d69d 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -456,6 +456,7 @@ static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStr 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 9842e253f063..f13f82da3102 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -378,6 +378,7 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table // 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 ad5ed4db55b6..591ba14942cd 100644 --- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp @@ -50,6 +50,53 @@ ResourceEntry* GetEntry(ResourceTable* table, const ResourceNameRef& res_name, R 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 995495ac56a8..d3648c8aae52 100644 --- a/tools/aapt2/java/ClassDefinition.h +++ b/tools/aapt2/java/ClassDefinition.h @@ -59,8 +59,9 @@ class ClassMember { 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 @@ class PrimitiveMember : public ClassMember { 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 @@ class PrimitiveMember : public ClassMember { 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 039448e1b3e9..e1e2e0135cf7 100644 --- a/tools/aapt2/java/JavaClassGenerator.cpp +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -460,7 +460,8 @@ void JavaClassGenerator::ProcessResource(const ResourceNameRef& name, const Reso 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 de72334e65a9..d385267fe5ed 100644 --- a/tools/aapt2/process/SymbolTable.cpp +++ b/tools/aapt2/process/SymbolTable.cpp @@ -199,7 +199,8 @@ std::unique_ptr<SymbolTable::Symbol> ResourceTableSymbolSource::FindByName( 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 @@ std::unique_ptr<SymbolTable::Symbol> AssetManagerSymbolSource::FindByName( 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 @@ std::unique_ptr<SymbolTable::Symbol> AssetManagerSymbolSource::FindById( 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 {}; |