diff options
Diffstat (limited to 'tools')
163 files changed, 10389 insertions, 3120 deletions
diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp index 05375b0cb871..21386b88ce2c 100644 --- a/tools/aapt/Command.cpp +++ b/tools/aapt/Command.cpp @@ -787,7 +787,9 @@ int doDump(Bundle* bundle) // The dynamicRefTable can be null if there are no resources for this asset cookie. // This fine. - const DynamicRefTable* dynamicRefTable = res.getDynamicRefTableForCookie(assetsCookie); + auto noop_destructor = [](const DynamicRefTable* /*ref_table */) { }; + auto dynamicRefTable = std::shared_ptr<const DynamicRefTable>( + res.getDynamicRefTableForCookie(assetsCookie), noop_destructor); Asset* asset = NULL; diff --git a/tools/aapt/SdkConstants.h b/tools/aapt/SdkConstants.h index 27ffcdf52168..04fbbe1f1069 100644 --- a/tools/aapt/SdkConstants.h +++ b/tools/aapt/SdkConstants.h @@ -45,6 +45,7 @@ enum { SDK_O_MR1 = 27, SDK_P = 28, SDK_Q = 29, + SDK_R = 30, }; #endif // H_AAPT_SDK_CONSTANTS diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index a2709bd545b2..ade0dc4d9c42 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -57,9 +57,10 @@ cc_defaults { "libziparchive", "libpng", "libbase", - "libprotobuf-cpp-lite", + "libprotobuf-cpp-full", "libz", "libbuildversion", + "libidmap2_policies", ], stl: "libc++_static", group_static_libs: true, @@ -198,6 +199,7 @@ cc_test_host { cc_binary_host { name: "aapt2", srcs: ["Main.cpp"] + toolSources, + use_version_lib: true, static_libs: ["libaapt2"], defaults: ["aapt2_defaults"], } @@ -210,6 +212,7 @@ genrule { tools: [":soong_zip"], srcs: [ "Configuration.proto", + "ResourcesInternal.proto", "Resources.proto", ], out: ["aapt2-protos.zip"], diff --git a/tools/aapt2/AppInfo.h b/tools/aapt2/AppInfo.h index 75123537116f..d3ca357b0305 100644 --- a/tools/aapt2/AppInfo.h +++ b/tools/aapt2/AppInfo.h @@ -17,6 +17,7 @@ #ifndef AAPT_APP_INFO_H #define AAPT_APP_INFO_H +#include <set> #include <string> #include "util/Maybe.h" @@ -42,6 +43,9 @@ struct AppInfo { // The app's split name, if it is a split. Maybe<std::string> split_name; + + // The split names that this split depends on. + std::set<std::string> split_name_dependencies; }; } // namespace aapt diff --git a/tools/aapt2/Configuration.proto b/tools/aapt2/Configuration.proto index fc636a43ec40..8a4644c9a219 100644 --- a/tools/aapt2/Configuration.proto +++ b/tools/aapt2/Configuration.proto @@ -19,7 +19,6 @@ syntax = "proto3"; package aapt.pb; option java_package = "com.android.aapt"; -option optimize_for = LITE_RUNTIME; // A description of the requirements a device must have in order for a // resource to be matched and selected. diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index 3da22b4fb9fa..439f231193df 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -32,10 +32,16 @@ #include "text/Printer.h" #include "util/Util.h" +#include "idmap2/Policies.h" + using ::aapt::text::Printer; using ::android::StringPiece; using ::android::base::StringPrintf; +using android::idmap2::policy::kPolicyStringToFlag; + +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { namespace { @@ -178,19 +184,17 @@ class ValueBodyPrinter : public ConstValueVisitor { void Visit(const Array* array) override { const size_t count = array->elements.size(); printer_->Print("["); - if (count > 0) { - for (size_t i = 0u; i < count; i++) { - if (i != 0u && i % 4u == 0u) { - printer_->Println(); - printer_->Print(" "); - } - PrintItem(*array->elements[i]); - if (i != count - 1) { - printer_->Print(", "); - } + for (size_t i = 0u; i < count; i++) { + if (i != 0u && i % 4u == 0u) { + printer_->Println(); + printer_->Print(" "); + } + PrintItem(*array->elements[i]); + if (i != count - 1) { + printer_->Print(", "); } - printer_->Println("]"); } + printer_->Println("]"); } void Visit(const Plural* plural) override { @@ -314,6 +318,10 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& break; } + if (entry->overlayable_item) { + printer->Print(" OVERLAYABLE"); + } + printer->Println(); if (options.show_values) { @@ -527,4 +535,62 @@ void Debug::DumpXml(const xml::XmlResource& doc, Printer* printer) { doc.root->Accept(&xml_visitor); } +struct DumpOverlayableEntry { + std::string overlayable_section; + std::string policy_subsection; + std::string resource_name; +}; + +void Debug::DumpOverlayable(const ResourceTable& table, text::Printer* printer) { + std::vector<DumpOverlayableEntry> items; + for (const auto& package : table.packages) { + for (const auto& type : package->types) { + for (const auto& entry : type->entries) { + if (entry->overlayable_item) { + const auto& overlayable_item = entry->overlayable_item.value(); + const auto overlayable_section = StringPrintf(R"(name="%s" actor="%s")", + overlayable_item.overlayable->name.c_str(), + overlayable_item.overlayable->actor.c_str()); + const auto policy_subsection = StringPrintf(R"(policies="%s")", + android::idmap2::policy::PoliciesToDebugString(overlayable_item.policies).c_str()); + const auto value = + StringPrintf("%s/%s", to_string(type->type).data(), entry->name.c_str()); + items.push_back(DumpOverlayableEntry{overlayable_section, policy_subsection, value}); + } + } + } + } + + std::sort(items.begin(), items.end(), + [](const DumpOverlayableEntry& a, const DumpOverlayableEntry& b) { + if (a.overlayable_section != b.overlayable_section) { + return a.overlayable_section < b.overlayable_section; + } + if (a.policy_subsection != b.policy_subsection) { + return a.policy_subsection < b.policy_subsection; + } + return a.resource_name < b.resource_name; + }); + + std::string last_overlayable_section; + std::string last_policy_subsection; + for (const auto& item : items) { + if (last_overlayable_section != item.overlayable_section) { + printer->Println(item.overlayable_section); + last_overlayable_section = item.overlayable_section; + } + if (last_policy_subsection != item.policy_subsection) { + printer->Indent(); + printer->Println(item.policy_subsection); + last_policy_subsection = item.policy_subsection; + printer->Undent(); + } + printer->Indent(); + printer->Indent(); + printer->Println(item.resource_name); + printer->Undent(); + printer->Undent(); + } +} + } // namespace aapt diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h index a43197cacf7b..9443d606d7e5 100644 --- a/tools/aapt2/Debug.h +++ b/tools/aapt2/Debug.h @@ -39,6 +39,7 @@ struct Debug { static void DumpHex(const void* data, size_t len); static void DumpXml(const xml::XmlResource& doc, text::Printer* printer); static void DumpResStringPool(const android::ResStringPool* pool, text::Printer* printer); + static void DumpOverlayable(const ResourceTable& table, text::Printer* printer); }; } // namespace aapt diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index 67ba895e51d1..c49c370bcc44 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -147,10 +147,11 @@ struct ResourceId { ResourceId(uint32_t res_id); // NOLINT(google-explicit-constructor) ResourceId(uint8_t p, uint8_t t, uint16_t e); - bool is_valid() const; + // Returns true if the ID is a valid ID that is not dynamic (package ID cannot be 0) + bool is_valid_static() const; // Returns true if the ID is a valid ID or dynamic ID (package ID can be 0). - bool is_valid_dynamic() const; + bool is_valid() const; uint8_t package_id() const; uint8_t type_id() const; @@ -233,11 +234,11 @@ inline ResourceId::ResourceId(uint32_t res_id) : id(res_id) {} inline ResourceId::ResourceId(uint8_t p, uint8_t t, uint16_t e) : id((p << 24) | (t << 16) | e) {} -inline bool ResourceId::is_valid() const { +inline bool ResourceId::is_valid_static() const { return (id & 0xff000000u) != 0 && (id & 0x00ff0000u) != 0; } -inline bool ResourceId::is_valid_dynamic() const { +inline bool ResourceId::is_valid() const { return (id & 0x00ff0000u) != 0; } diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index b34c3d59d6a5..931a14b1f650 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -32,11 +32,15 @@ #include "util/Util.h" #include "xml/XmlPullParser.h" +#include "idmap2/Policies.h" + using ::aapt::ResourceUtils::StringBuilder; using ::aapt::text::Utf8Iterator; using ::android::ConfigDescription; using ::android::StringPiece; +using android::idmap2::policy::kPolicyStringToFlag; + namespace aapt { constexpr const char* sXliffNamespaceUri = "urn:oasis:names:tc:xliff:document:1.2"; @@ -770,16 +774,14 @@ std::unique_ptr<Item> ResourceParser::ParseXml(xml::XmlPullParser* parser, return std::move(string); } - // If the text is empty, and the value is not allowed to be a string, encode it as a @null. - if (util::TrimWhitespace(raw_value).empty()) { - return ResourceUtils::MakeNull(); - } - if (allow_raw_value) { // We can't parse this so return a RawString if we are allowed. return util::make_unique<RawString>( table_->string_pool.MakeRef(util::TrimWhitespace(raw_value), StringPool::Context(config_))); + } else if (util::TrimWhitespace(raw_value).empty()) { + // If the text is empty, and the value is not allowed to be a string, encode it as a @null. + return ResourceUtils::MakeNull(); } return {}; } @@ -1067,7 +1069,7 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource bool error = false; std::string comment; - OverlayableItem::PolicyFlags current_policies = OverlayableItem::Policy::kNone; + PolicyFlags current_policies = PolicyFlags::NONE; const size_t start_depth = parser->depth(); while (xml::XmlPullParser::IsGoodEvent(parser->Next())) { xml::XmlPullParser::Event event = parser->event(); @@ -1077,7 +1079,7 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource } else if (event == xml::XmlPullParser::Event::kEndElement && parser->depth() == start_depth + 1) { // Clear the current policies when exiting the <policy> tags - current_policies = OverlayableItem::Policy::kNone; + current_policies = PolicyFlags::NONE; continue; } else if (event == xml::XmlPullParser::Event::kComment) { // Retrieve the comment of individual <item> tags @@ -1092,7 +1094,7 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource const std::string& element_name = parser->element_name(); const std::string& element_namespace = parser->element_namespace(); if (element_namespace.empty() && element_name == "item") { - if (current_policies == OverlayableItem::Policy::kNone) { + if (current_policies == PolicyFlags::NONE) { diag_->Error(DiagMessage(element_source) << "<item> within an <overlayable> must be inside a <policy> block"); error = true; @@ -1137,7 +1139,7 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource out_resource->child_resources.push_back(std::move(child_resource)); } else if (element_namespace.empty() && element_name == "policy") { - if (current_policies != OverlayableItem::Policy::kNone) { + if (current_policies != PolicyFlags::NONE) { // If the policy list is not empty, then we are currently inside a policy element diag_->Error(DiagMessage(element_source) << "<policy> blocks cannot be recursively nested"); error = true; @@ -1145,21 +1147,14 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource } else if (Maybe<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) { // Parse the polices separated by vertical bar characters to allow for specifying multiple // policies. Items within the policy tag will have the specified policy. - static const auto kPolicyMap = - ImmutableMap<StringPiece, OverlayableItem::Policy>::CreatePreSorted({ - {"odm", OverlayableItem::Policy::kOdm}, - {"oem", OverlayableItem::Policy::kOem}, - {"product", OverlayableItem::Policy::kProduct}, - {"public", OverlayableItem::Policy::kPublic}, - {"signature", OverlayableItem::Policy::kSignature}, - {"system", OverlayableItem::Policy::kSystem}, - {"vendor", OverlayableItem::Policy::kVendor}, - }); - for (const StringPiece& part : util::Tokenize(maybe_type.value(), '|')) { StringPiece trimmed_part = util::TrimWhitespace(part); - const auto policy = kPolicyMap.find(trimmed_part); - if (policy == kPolicyMap.end()) { + const auto policy = std::find_if(kPolicyStringToFlag.begin(), + kPolicyStringToFlag.end(), + [trimmed_part](const auto& it) { + return trimmed_part == it.first; + }); + if (policy == kPolicyStringToFlag.end()) { diag_->Error(DiagMessage(element_source) << "<policy> has unsupported type '" << trimmed_part << "'"); error = true; @@ -1391,7 +1386,7 @@ Maybe<Attribute::Symbol> ResourceParser::ParseEnumOrFlagItem( return Attribute::Symbol{ Reference(ResourceNameRef({}, ResourceType::kId, maybe_name.value())), - val.data}; + val.data, val.dataType}; } bool ResourceParser::ParseStyleItem(xml::XmlPullParser* parser, Style* style) { diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index 46ad7cbe49e9..9b70079a98c9 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -41,6 +41,8 @@ using ::testing::Pointee; using ::testing::SizeIs; using ::testing::StrEq; +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; @@ -401,7 +403,7 @@ TEST_F(ResourceParserTest, ParseEnumAttr) { std::string input = R"( <attr name="foo"> <enum name="bar" value="0"/> - <enum name="bat" value="1"/> + <enum name="bat" value="0x1"/> <enum name="baz" value="2"/> </attr>)"; ASSERT_TRUE(TestParse(input)); @@ -414,14 +416,17 @@ TEST_F(ResourceParserTest, ParseEnumAttr) { ASSERT_TRUE(enum_attr->symbols[0].symbol.name); EXPECT_THAT(enum_attr->symbols[0].symbol.name.value().entry, Eq("bar")); EXPECT_THAT(enum_attr->symbols[0].value, Eq(0u)); + EXPECT_THAT(enum_attr->symbols[0].type, Eq(Res_value::TYPE_INT_DEC)); ASSERT_TRUE(enum_attr->symbols[1].symbol.name); EXPECT_THAT(enum_attr->symbols[1].symbol.name.value().entry, Eq("bat")); EXPECT_THAT(enum_attr->symbols[1].value, Eq(1u)); + EXPECT_THAT(enum_attr->symbols[1].type, Eq(Res_value::TYPE_INT_HEX)); ASSERT_TRUE(enum_attr->symbols[2].symbol.name); EXPECT_THAT(enum_attr->symbols[2].symbol.name.value().entry, Eq("baz")); EXPECT_THAT(enum_attr->symbols[2].value, Eq(2u)); + EXPECT_THAT(enum_attr->symbols[2].type, Eq(Res_value::TYPE_INT_DEC)); } TEST_F(ResourceParserTest, ParseFlagAttr) { @@ -956,7 +961,7 @@ TEST_F(ResourceParserTest, ParseOverlayable) { OverlayableItem& result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); EXPECT_THAT(result_overlayable_item.overlayable->actor, Eq("overlay://theme")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kSignature)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::SIGNATURE)); search_result = table_.FindResource(test::ParseNameOrDie("drawable/bar")); ASSERT_TRUE(search_result); @@ -965,7 +970,7 @@ TEST_F(ResourceParserTest, ParseOverlayable) { result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); EXPECT_THAT(result_overlayable_item.overlayable->actor, Eq("overlay://theme")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kSignature)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::SIGNATURE)); } TEST_F(ResourceParserTest, ParseOverlayableRequiresName) { @@ -1002,6 +1007,9 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { <policy type="oem"> <item type="string" name="buz" /> </policy> + <policy type="actor"> + <item type="string" name="actor" /> + </policy> </overlayable>)"; ASSERT_TRUE(TestParse(input)); @@ -1011,7 +1019,7 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); OverlayableItem result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kProduct)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::PRODUCT_PARTITION)); search_result = table_.FindResource(test::ParseNameOrDie("string/fiz")); ASSERT_TRUE(search_result); @@ -1019,7 +1027,7 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kSystem)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::SYSTEM_PARTITION)); search_result = table_.FindResource(test::ParseNameOrDie("string/fuz")); ASSERT_TRUE(search_result); @@ -1027,7 +1035,7 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kVendor)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::VENDOR_PARTITION)); search_result = table_.FindResource(test::ParseNameOrDie("string/faz")); ASSERT_TRUE(search_result); @@ -1035,7 +1043,7 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kPublic)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::PUBLIC)); search_result = table_.FindResource(test::ParseNameOrDie("string/foz")); ASSERT_TRUE(search_result); @@ -1043,7 +1051,7 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kSignature)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::SIGNATURE)); search_result = table_.FindResource(test::ParseNameOrDie("string/biz")); ASSERT_TRUE(search_result); @@ -1051,7 +1059,7 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kOdm)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::ODM_PARTITION)); search_result = table_.FindResource(test::ParseNameOrDie("string/buz")); ASSERT_TRUE(search_result); @@ -1059,7 +1067,15 @@ TEST_F(ResourceParserTest, ParseOverlayablePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kOem)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::OEM_PARTITION)); + + search_result = table_.FindResource(test::ParseNameOrDie("string/actor")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + ASSERT_TRUE(search_result.value().entry->overlayable_item); + result_overlayable_item = search_result.value().entry->overlayable_item.value(); + EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::ACTOR_SIGNATURE)); } TEST_F(ResourceParserTest, ParseOverlayableNoPolicyError) { @@ -1122,8 +1138,8 @@ TEST_F(ResourceParserTest, ParseOverlayableMultiplePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); OverlayableItem result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kVendor - | OverlayableItem::Policy::kPublic)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::VENDOR_PARTITION + | PolicyFlags::PUBLIC)); search_result = table_.FindResource(test::ParseNameOrDie("string/bar")); ASSERT_TRUE(search_result); @@ -1131,8 +1147,8 @@ TEST_F(ResourceParserTest, ParseOverlayableMultiplePolicy) { ASSERT_TRUE(search_result.value().entry->overlayable_item); result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("Name")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kProduct - | OverlayableItem::Policy::kSystem)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::PRODUCT_PARTITION + | PolicyFlags::SYSTEM_PARTITION)); } TEST_F(ResourceParserTest, DuplicateOverlayableIsError) { diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 836e199593fc..e0a9a31eee8b 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -267,8 +267,7 @@ bool ResourceEntry::HasDefaultValue() const { // A DECL will override a USE without error. Two DECLs must match in their format for there to be // no error. ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* existing, - Value* incoming, - bool overlay) { + Value* incoming) { Attribute* existing_attr = ValueCast<Attribute>(existing); Attribute* incoming_attr = ValueCast<Attribute>(incoming); if (!incoming_attr) { @@ -282,7 +281,7 @@ ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* exist } // The existing and incoming values are strong, this is an error // if the values are not both attributes. - return overlay ? CollisionResult::kTakeNew : CollisionResult::kConflict; + return CollisionResult::kConflict; } if (!existing_attr) { @@ -293,7 +292,7 @@ ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* exist } // The existing value is not an attribute and it is strong, // so the incoming attribute value is an error. - return overlay ? CollisionResult::kTakeNew : CollisionResult::kConflict; + return CollisionResult::kConflict; } CHECK(incoming_attr != nullptr && existing_attr != nullptr); @@ -324,9 +323,8 @@ ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* exist return CollisionResult::kConflict; } -ResourceTable::CollisionResult ResourceTable::IgnoreCollision(Value* /* existing */, - Value* /* incoming */, - bool /* overlay */) { +ResourceTable::CollisionResult ResourceTable::IgnoreCollision(Value* /** existing **/, + Value* /** incoming **/) { return CollisionResult::kKeepBoth; } @@ -400,7 +398,7 @@ bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceI // Check for package names appearing twice with two different package ids ResourceTablePackage* package = FindOrCreatePackage(name.package); - if (res_id.is_valid_dynamic() && package->id && package->id.value() != res_id.package_id()) { + if (res_id.is_valid() && package->id && package->id.value() != res_id.package_id()) { diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id << " but package '" << package->name << "' already has ID " @@ -409,9 +407,9 @@ bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceI } // Whether or not to error on duplicate resources - bool check_id = validate_resources_ && res_id.is_valid_dynamic(); + bool check_id = validate_resources_ && res_id.is_valid(); // Whether or not to create a duplicate resource if the id does not match - bool use_id = !validate_resources_ && res_id.is_valid_dynamic(); + bool use_id = !validate_resources_ && res_id.is_valid(); ResourceTableType* type = package->FindOrCreateType(name.type, use_id ? res_id.type_id() : Maybe<uint8_t>()); @@ -442,7 +440,7 @@ bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceI // Resource does not exist, add it now. config_value->value = std::move(value); } else { - switch (conflict_resolver(config_value->value.get(), value.get(), false /* overlay */)) { + switch (conflict_resolver(config_value->value.get(), value.get())) { case CollisionResult::kKeepBoth: // Insert the value ignoring for duplicate configurations entry->values.push_back(util::make_unique<ResourceConfigValue>(config, product)); @@ -465,7 +463,7 @@ bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceI } } - if (res_id.is_valid_dynamic()) { + if (res_id.is_valid()) { package->id = res_id.package_id(); type->id = res_id.type_id(); entry->id = res_id.entry_id(); @@ -506,7 +504,7 @@ bool ResourceTable::SetVisibilityImpl(const ResourceNameRef& name, const Visibil // Check for package names appearing twice with two different package ids ResourceTablePackage* package = FindOrCreatePackage(name.package); - if (res_id.is_valid_dynamic() && package->id && package->id.value() != res_id.package_id()) { + if (res_id.is_valid() && package->id && package->id.value() != res_id.package_id()) { diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id << " but package '" << package->name << "' already has ID " @@ -515,9 +513,9 @@ bool ResourceTable::SetVisibilityImpl(const ResourceNameRef& name, const Visibil } // Whether or not to error on duplicate resources - bool check_id = validate_resources_ && res_id.is_valid_dynamic(); + bool check_id = validate_resources_ && res_id.is_valid(); // Whether or not to create a duplicate resource if the id does not match - bool use_id = !validate_resources_ && res_id.is_valid_dynamic(); + bool use_id = !validate_resources_ && res_id.is_valid(); ResourceTableType* type = package->FindOrCreateType(name.type, use_id ? res_id.type_id() : Maybe<uint8_t>()); @@ -543,7 +541,7 @@ bool ResourceTable::SetVisibilityImpl(const ResourceNameRef& name, const Visibil return false; } - if (res_id.is_valid_dynamic()) { + if (res_id.is_valid()) { package->id = res_id.package_id(); type->id = res_id.type_id(); entry->id = res_id.entry_id(); diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index e8793800b148..93a7a314d6ba 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -36,6 +36,8 @@ #include <unordered_map> #include <vector> +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { // The Public status of a resource. @@ -75,36 +77,8 @@ struct Overlayable { struct OverlayableItem { explicit OverlayableItem(const std::shared_ptr<Overlayable>& overlayable) : overlayable(overlayable) {} - - // Represents the types overlays that are allowed to overlay the resource. - typedef uint32_t PolicyFlags; - enum Policy : uint32_t { - kNone = 0x00000000, - - // The resource can be overlaid by any overlay. - kPublic = 0x00000001, - - // The resource can be overlaid by any overlay on the system partition. - kSystem = 0x00000002, - - // The resource can be overlaid by any overlay on the vendor partition. - kVendor = 0x00000004, - - // The resource can be overlaid by any overlay on the product partition. - kProduct = 0x00000008, - - // The resource can be overlaid by any overlay signed with the same signature as its actor. - kSignature = 0x00000010, - - // The resource can be overlaid by any overlay on the odm partition. - kOdm = 0x00000020, - - // The resource can be overlaid by any overlay on the oem partition. - kOem = 0x00000040, - }; - std::shared_ptr<Overlayable> overlayable; - PolicyFlags policies = Policy::kNone; + PolicyFlags policies = PolicyFlags::NONE; std::string comment; Source source; }; @@ -228,13 +202,13 @@ class ResourceTable { enum class CollisionResult { kKeepBoth, kKeepOriginal, kConflict, kTakeNew }; - using CollisionResolverFunc = std::function<CollisionResult(Value*, Value*, bool)>; + using CollisionResolverFunc = std::function<CollisionResult(Value*, Value*)>; // When a collision of resources occurs, this method decides which value to keep. - static CollisionResult ResolveValueCollision(Value* existing, Value* incoming, bool overlay); + static CollisionResult ResolveValueCollision(Value* existing, Value* incoming); // When a collision of resources occurs, this method keeps both values - static CollisionResult IgnoreCollision(Value* existing, Value* incoming, bool overlay); + static CollisionResult IgnoreCollision(Value* existing, Value* incoming); bool AddResource(const ResourceNameRef& name, const android::ConfigDescription& config, const android::StringPiece& product, std::unique_ptr<Value> value, diff --git a/tools/aapt2/ResourceTable_test.cpp b/tools/aapt2/ResourceTable_test.cpp index b97dc6b205ca..9271a7e6bae1 100644 --- a/tools/aapt2/ResourceTable_test.cpp +++ b/tools/aapt2/ResourceTable_test.cpp @@ -30,6 +30,8 @@ using ::testing::Eq; using ::testing::NotNull; using ::testing::StrEq; +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { TEST(ResourceTableTest, FailToAddResourceWithBadName) { @@ -247,8 +249,8 @@ TEST(ResourceTableTest, SetOverlayable) { auto overlayable = std::make_shared<Overlayable>("Name", "overlay://theme", Source("res/values/overlayable.xml", 40)); OverlayableItem overlayable_item(overlayable); - overlayable_item.policies |= OverlayableItem::Policy::kProduct; - overlayable_item.policies |= OverlayableItem::Policy::kVendor; + overlayable_item.policies |= PolicyFlags::PRODUCT_PARTITION; + overlayable_item.policies |= PolicyFlags::VENDOR_PARTITION; overlayable_item.comment = "comment"; overlayable_item.source = Source("res/values/overlayable.xml", 42); @@ -264,8 +266,8 @@ TEST(ResourceTableTest, SetOverlayable) { EXPECT_THAT(result_overlayable_item.overlayable->actor, Eq("overlay://theme")); EXPECT_THAT(result_overlayable_item.overlayable->source.path, Eq("res/values/overlayable.xml")); EXPECT_THAT(result_overlayable_item.overlayable->source.line, 40); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kProduct - | OverlayableItem::Policy::kVendor)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::PRODUCT_PARTITION + | PolicyFlags::VENDOR_PARTITION)); ASSERT_THAT(result_overlayable_item.comment, StrEq("comment")); EXPECT_THAT(result_overlayable_item.source.path, Eq("res/values/overlayable.xml")); EXPECT_THAT(result_overlayable_item.source.line, 42); @@ -277,17 +279,17 @@ TEST(ResourceTableTest, SetMultipleOverlayableResources) { const ResourceName foo = test::ParseNameOrDie("android:string/foo"); auto group = std::make_shared<Overlayable>("Name", "overlay://theme"); OverlayableItem overlayable(group); - overlayable.policies = OverlayableItem::Policy::kProduct; + overlayable.policies = PolicyFlags::PRODUCT_PARTITION; ASSERT_TRUE(table.SetOverlayable(foo, overlayable, test::GetDiagnostics())); const ResourceName bar = test::ParseNameOrDie("android:string/bar"); OverlayableItem overlayable2(group); - overlayable2.policies = OverlayableItem::Policy::kProduct; + overlayable2.policies = PolicyFlags::PRODUCT_PARTITION; ASSERT_TRUE(table.SetOverlayable(bar, overlayable2, test::GetDiagnostics())); const ResourceName baz = test::ParseNameOrDie("android:string/baz"); OverlayableItem overlayable3(group); - overlayable3.policies = OverlayableItem::Policy::kVendor; + overlayable3.policies = PolicyFlags::VENDOR_PARTITION; ASSERT_TRUE(table.SetOverlayable(baz, overlayable3, test::GetDiagnostics())); } @@ -296,12 +298,12 @@ TEST(ResourceTableTest, SetOverlayableDifferentResourcesDifferentName) { const ResourceName foo = test::ParseNameOrDie("android:string/foo"); OverlayableItem overlayable_item(std::make_shared<Overlayable>("Name", "overlay://theme")); - overlayable_item.policies = OverlayableItem::Policy::kProduct; + overlayable_item.policies = PolicyFlags::PRODUCT_PARTITION; ASSERT_TRUE(table.SetOverlayable(foo, overlayable_item, test::GetDiagnostics())); const ResourceName bar = test::ParseNameOrDie("android:string/bar"); OverlayableItem overlayable_item2(std::make_shared<Overlayable>("Name2", "overlay://theme")); - overlayable_item2.policies = OverlayableItem::Policy::kProduct; + overlayable_item2.policies = PolicyFlags::PRODUCT_PARTITION; ASSERT_TRUE(table.SetOverlayable(bar, overlayable_item2, test::GetDiagnostics())); } diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp index e0040e486a23..469128b1e50b 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -378,7 +378,7 @@ std::unique_ptr<BinaryPrimitive> TryParseEnumSymbol(const Attribute* enum_attr, const ResourceName& enum_symbol_resource_name = symbol.symbol.name.value(); if (trimmed_str == enum_symbol_resource_name.entry) { android::Res_value value = {}; - value.dataType = android::Res_value::TYPE_INT_DEC; + value.dataType = symbol.type; value.data = symbol.value; return util::make_unique<BinaryPrimitive>(value); } @@ -516,7 +516,7 @@ Maybe<ResourceId> ParseResourceId(const StringPiece& str) { if (android::ResTable::stringToInt(str16.data(), str16.size(), &value)) { if (value.dataType == android::Res_value::TYPE_INT_HEX) { ResourceId id(value.data); - if (id.is_valid_dynamic()) { + if (id.is_valid()) { return id; } } @@ -738,7 +738,13 @@ std::unique_ptr<Item> ParseBinaryResValue(const ResourceType& type, const Config const android::Res_value& res_value, StringPool* dst_pool) { if (type == ResourceType::kId) { - return util::make_unique<Id>(); + if (res_value.dataType != android::Res_value::TYPE_REFERENCE && + res_value.dataType != android::Res_value::TYPE_DYNAMIC_REFERENCE) { + // plain "id" resources are actually encoded as dummy values (aapt1 uses an empty string, + // while aapt2 uses a false boolean). + return util::make_unique<Id>(); + } + // fall through to regular reference deserialization logic } const uint32_t data = util::DeviceToHost32(res_value.data); @@ -794,7 +800,12 @@ std::unique_ptr<Item> ParseBinaryResValue(const ResourceType& type, const Config } // This is a normal reference. - return util::make_unique<Reference>(data, ref_type); + auto reference = util::make_unique<Reference>(data, ref_type); + if (res_value.dataType == android::Res_value::TYPE_DYNAMIC_REFERENCE || + res_value.dataType == android::Res_value::TYPE_DYNAMIC_ATTRIBUTE) { + reference->is_dynamic = true; + } + return reference; } break; } diff --git a/tools/aapt2/ResourceUtils_test.cpp b/tools/aapt2/ResourceUtils_test.cpp index c016cb44af00..b08bf9a1ff17 100644 --- a/tools/aapt2/ResourceUtils_test.cpp +++ b/tools/aapt2/ResourceUtils_test.cpp @@ -109,6 +109,20 @@ TEST(ResourceUtilsTest, ParsePrivateReference) { EXPECT_TRUE(private_ref); } +TEST(ResourceUtilsTest, ParseBinaryDynamicReference) { + android::Res_value value = {}; + value.data = util::HostToDevice32(0x01); + value.dataType = android::Res_value::TYPE_DYNAMIC_REFERENCE; + std::unique_ptr<Item> item = ResourceUtils::ParseBinaryResValue(ResourceType::kId, + android::ConfigDescription(), + android::ResStringPool(), value, + nullptr); + + Reference* ref = ValueCast<Reference>(item.get()); + EXPECT_TRUE(ref->is_dynamic); + EXPECT_EQ(ref->id.value().id, 0x01); +} + TEST(ResourceUtilsTest, FailToParseAutoCreateNonIdReference) { bool create = false; bool private_ref = false; diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp index 696012786e6d..4f0fa8ae29ba 100644 --- a/tools/aapt2/ResourceValues.cpp +++ b/tools/aapt2/ResourceValues.cpp @@ -117,7 +117,7 @@ bool Reference::Equals(const Value* value) const { bool Reference::Flatten(android::Res_value* out_value) const { const ResourceId resid = id.value_or_default(ResourceId(0)); - const bool dynamic = resid.is_valid_dynamic() && is_dynamic; + const bool dynamic = resid.is_valid() && is_dynamic; if (reference_type == Reference::Type::kResource) { if (dynamic) { @@ -159,7 +159,7 @@ void Reference::Print(std::ostream* out) const { *out << name.value(); } - if (id && id.value().is_valid_dynamic()) { + if (id && id.value().is_valid()) { if (name) { *out << " "; } @@ -196,7 +196,7 @@ static void PrettyPrintReferenceImpl(const Reference& ref, bool print_package, P printer->Print("/"); printer->Print(name.entry); } - } else if (ref.id && ref.id.value().is_valid_dynamic()) { + } else if (ref.id && ref.id.value().is_valid()) { printer->Print(ref.id.value().to_string()); } } @@ -574,10 +574,6 @@ bool Attribute::Equals(const Value* value) const { } bool Attribute::IsCompatibleWith(const Attribute& attr) const { - if (Equals(&attr)) { - return true; - } - // If the high bits are set on any of these attribute type masks, then they are incompatible. // We don't check that flags and enums are identical. if ((type_mask & ~android::ResTable_map::TYPE_ANY) != 0 || diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h index 168ad61784e7..fe0883be50aa 100644 --- a/tools/aapt2/ResourceValues.h +++ b/tools/aapt2/ResourceValues.h @@ -292,6 +292,7 @@ struct Attribute : public BaseValue<Attribute> { struct Symbol { Reference symbol; uint32_t value; + uint8_t type; friend std::ostream& operator<<(std::ostream& out, const Symbol& symbol); }; diff --git a/tools/aapt2/ResourceValues_test.cpp b/tools/aapt2/ResourceValues_test.cpp index dbf51143f720..c4a1108ac62a 100644 --- a/tools/aapt2/ResourceValues_test.cpp +++ b/tools/aapt2/ResourceValues_test.cpp @@ -284,58 +284,8 @@ TEST(ResourcesValuesTest, AttributeIsCompatible) { EXPECT_FALSE(attr_three.IsCompatibleWith(attr_one)); EXPECT_FALSE(attr_three.IsCompatibleWith(attr_two)); - EXPECT_TRUE(attr_three.IsCompatibleWith(attr_three)); + EXPECT_FALSE(attr_three.IsCompatibleWith(attr_three)); EXPECT_FALSE(attr_three.IsCompatibleWith(attr_four)); - - EXPECT_FALSE(attr_four.IsCompatibleWith(attr_one)); - EXPECT_FALSE(attr_four.IsCompatibleWith(attr_two)); - EXPECT_FALSE(attr_four.IsCompatibleWith(attr_three)); - EXPECT_TRUE(attr_four.IsCompatibleWith(attr_four)); -} - -TEST(ResourcesValuesTest, AttributeEnumIsCompatible) { - Attribute attr_one(TYPE_ENUM); - attr_one.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/foo")), 0x01u}); - attr_one.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/bar")), 0x07u}); - - Attribute attr_two(TYPE_ENUM); - attr_two.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/foo")), 0x01u}); - attr_two.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/bar")), 0x07u}); - EXPECT_TRUE(attr_one.IsCompatibleWith(attr_two)); -} - -TEST(ResourcesValuesTest, DifferentAttributeEnumDifferentNameIsNotCompatible) { - Attribute attr_one(TYPE_ENUM); - attr_one.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/foo")), 0x01u}); - attr_one.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/bar")), 0x07u}); - - Attribute attr_two(TYPE_ENUM); - attr_two.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/foo")), 0x01u}); - attr_one.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/baz")), 0x07u}); - EXPECT_FALSE(attr_one.IsCompatibleWith(attr_two)); -} - -TEST(ResourcesValuesTest, DifferentAttributeEnumDifferentValueIsNotCompatible) { - Attribute attr_one(TYPE_ENUM); - attr_one.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/foo")), 0x01u}); - attr_one.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/bar")), 0x07u}); - - Attribute attr_two(TYPE_ENUM); - attr_two.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/foo")), 0x01u}); - attr_two.symbols.push_back( - Attribute::Symbol{Reference(test::ParseNameOrDie("android:id/bar")), 0x09u}); - EXPECT_FALSE(attr_one.IsCompatibleWith(attr_two)); } } // namespace aapt diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto index b2fc08423d34..ab9ce66b0ae3 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -21,7 +21,6 @@ import "frameworks/base/tools/aapt2/Configuration.proto"; package aapt.pb; option java_package = "com.android.aapt"; -option optimize_for = LITE_RUNTIME; // A string pool that wraps the binary form of the C++ class android::ResStringPool. message StringPool { @@ -168,6 +167,7 @@ message OverlayableItem { SIGNATURE = 5; ODM = 6; OEM = 7; + ACTOR = 8; } // The location of the <item> declaration in source. @@ -270,6 +270,11 @@ message CompoundValue { } } +// Message holding a boolean, so it can be optionally encoded. +message Boolean { + bool value = 1; +} + // A value that is a reference to another resource. This reference can be by name or resource ID. message Reference { enum Type { @@ -290,6 +295,9 @@ message Reference { // Whether this reference is referencing a private resource (@*package:type/entry). bool private = 4; + + // Whether this reference is dynamic. + Boolean is_dynamic = 5; } // A value that represents an ID. This is just a placeholder, as ID values are used to occupy a @@ -388,6 +396,9 @@ message Attribute { // The value of the enum/flag. uint32 value = 4; + + // The data type of the enum/flag as defined in android::Res_value. + uint32 type = 5; } // Bitmask of formats allowed for an attribute. diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto index 520b242ee509..b0ed3da33368 100644 --- a/tools/aapt2/ResourcesInternal.proto +++ b/tools/aapt2/ResourcesInternal.proto @@ -22,7 +22,6 @@ import "frameworks/base/tools/aapt2/Resources.proto"; package aapt.pb.internal; option java_package = "android.aapt.pb.internal"; -option optimize_for = LITE_RUNTIME; // The top level message representing an external resource file (layout XML, PNG, etc). // This is used to represent a compiled file before it is linked. Only useful to aapt2. diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp index 304bc4925831..e8873bf2d81b 100644 --- a/tools/aapt2/SdkConstants.cpp +++ b/tools/aapt2/SdkConstants.cpp @@ -57,6 +57,8 @@ static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = { {0x0568, SDK_O}, {0x056d, SDK_O_MR1}, {0x0586, SDK_P}, + {0x0606, SDK_Q}, + {0x0617, SDK_R}, }; static bool less_entry_id(const std::pair<uint16_t, ApiVersion>& p, uint16_t entryId) { diff --git a/tools/aapt2/SdkConstants.h b/tools/aapt2/SdkConstants.h index a00d978565ad..aa9aa12d2cee 100644 --- a/tools/aapt2/SdkConstants.h +++ b/tools/aapt2/SdkConstants.h @@ -55,6 +55,7 @@ enum : ApiVersion { SDK_O_MR1 = 27, SDK_P = 28, SDK_Q = 29, + SDK_R = 30, }; ApiVersion FindAttributeSdkLevel(const ResourceId& id); diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 21719705838d..32686538c10d 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -630,6 +630,12 @@ class CompileContext : public IAaptContext { return 0; } + const std::set<std::string>& GetSplitNameDependencies() override { + UNIMPLEMENTED(FATAL) << "No Split Name Dependencies be needed in compile phase"; + static std::set<std::string> empty; + return empty; + } + private: DISALLOW_COPY_AND_ASSIGN(CompileContext); @@ -735,7 +741,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) { } std::unique_ptr<io::IFileCollection> file_collection; - std::unique_ptr<IArchiveWriter> archive_writer; // Collect the resources files to compile if (options_.res_dir && options_.res_zip) { @@ -756,8 +761,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) { context.GetDiagnostics()->Error(DiagMessage(options_.res_dir.value()) << err); return 1; } - - archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); } else if (options_.res_zip) { if (!args.empty()) { context.GetDiagnostics()->Error(DiagMessage() << "files given but --zip specified"); @@ -772,8 +775,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) { context.GetDiagnostics()->Error(DiagMessage(options_.res_zip.value()) << err); return 1; } - - archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); } else { auto collection = util::make_unique<io::FileCollection>(); @@ -786,7 +787,14 @@ int CompileCommand::Action(const std::vector<std::string>& args) { } file_collection = std::move(collection); + } + + std::unique_ptr<IArchiveWriter> archive_writer; + file::FileType output_file_type = file::GetFileType(options_.output_path); + if (output_file_type == file::FileType::kDirectory) { archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options_.output_path); + } else { + archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path); } if (!archive_writer) { diff --git a/tools/aapt2/cmd/Compile_test.cpp b/tools/aapt2/cmd/Compile_test.cpp index 5f637bd8d582..fb786a31360e 100644 --- a/tools/aapt2/cmd/Compile_test.cpp +++ b/tools/aapt2/cmd/Compile_test.cpp @@ -200,7 +200,7 @@ static void AssertTranslations(CommandTestFixture *ctf, std::string file_name, const std::string compiled_files_dir = ctf->GetTestPath("/compiled_" + file_name); const std::string out_apk = ctf->GetTestPath("/" + file_name + ".apk"); - CHECK(ctf->WriteFile(source_file, sTranslatableXmlContent)); + ctf->WriteFile(source_file, sTranslatableXmlContent); CHECK(file::mkdirs(compiled_files_dir.data())); ASSERT_EQ(CompileCommand(&diag).Execute({ diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 0cf86ccdd59f..22bcd8589ce9 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -243,6 +243,12 @@ class Context : public IAaptContext { return 0u; } + const std::set<std::string>& GetSplitNameDependencies() override { + UNIMPLEMENTED(FATAL) << "Split Name Dependencies should not be necessary"; + static std::set<std::string> empty; + return empty; + } + bool verbose_ = false; std::string package_; diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp index 262f4fc4e394..d56994e3ae24 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -65,6 +65,12 @@ class DiffContext : public IAaptContext { return 0; } + const std::set<std::string>& GetSplitNameDependencies() override { + UNIMPLEMENTED(FATAL) << "Split Name Dependencies should not be necessary"; + static std::set<std::string> empty; + return empty; + } + private: std::string empty_; StdErrDiagnostics diagnostics_; diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp index a23a6a46cf0f..3982d12f6036 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -118,6 +118,12 @@ class DumpContext : public IAaptContext { return 0; } + const std::set<std::string>& GetSplitNameDependencies() override { + UNIMPLEMENTED(FATAL) << "Split Name Dependencies should not be necessary"; + static std::set<std::string> empty; + return empty; + } + private: StdErrDiagnostics diagnostics_; bool verbose_ = false; @@ -388,6 +394,17 @@ int DumpXmlTreeCommand::Dump(LoadedApk* apk) { return 0; } +int DumpOverlayableCommand::Dump(LoadedApk* apk) { + ResourceTable* table = apk->GetResourceTable(); + if (!table) { + GetDiagnostics()->Error(DiagMessage() << "Failed to retrieve resource table"); + return 1; + } + + Debug::DumpOverlayable(*table, GetPrinter()); + return 0; +} + const char DumpBadgerCommand::kBadgerData[2925] = { 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 95, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, diff --git a/tools/aapt2/cmd/Dump.h b/tools/aapt2/cmd/Dump.h index 7ded9bcf8470..cd51f7a7718c 100644 --- a/tools/aapt2/cmd/Dump.h +++ b/tools/aapt2/cmd/Dump.h @@ -240,6 +240,16 @@ class DumpXmlTreeCommand : public DumpApkCommand { std::vector<std::string> files_; }; +class DumpOverlayableCommand : public DumpApkCommand { + public: + explicit DumpOverlayableCommand(text::Printer* printer, IDiagnostics* diag) + : DumpApkCommand("overlayable", printer, diag) { + SetDescription("Print the <overlayable> resources of an APK."); + } + + int Dump(LoadedApk* apk) override; +}; + /** The default dump command. Performs no action because a subcommand is required. */ class DumpCommand : public Command { public: @@ -255,8 +265,8 @@ class DumpCommand : public Command { AddOptionalSubcommand(util::make_unique<DumpTableCommand>(printer, diag_)); AddOptionalSubcommand(util::make_unique<DumpXmlStringsCommand>(printer, diag_)); AddOptionalSubcommand(util::make_unique<DumpXmlTreeCommand>(printer, diag_)); + AddOptionalSubcommand(util::make_unique<DumpOverlayableCommand>(printer, diag_)); AddOptionalSubcommand(util::make_unique<DumpBadgerCommand>(printer), /* hidden */ true); - // TODO(b/120609160): Add aapt2 overlayable dump command } int Action(const std::vector<std::string>& args) override { diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 85414fb8a4e5..72cb41a1b172 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -140,6 +140,14 @@ class LinkContext : public IAaptContext { min_sdk_version_ = minSdk; } + const std::set<std::string>& GetSplitNameDependencies() override { + return split_name_dependencies_; + } + + void SetSplitNameDependencies(const std::set<std::string>& split_name_dependencies) { + split_name_dependencies_ = split_name_dependencies; + } + private: DISALLOW_COPY_AND_ASSIGN(LinkContext); @@ -151,6 +159,7 @@ class LinkContext : public IAaptContext { SymbolTable symbols_; bool verbose_ = false; int min_sdk_version_ = 0; + std::set<std::string> split_name_dependencies_; }; // A custom delegate that generates compatible pre-O IDs for use with feature splits. @@ -269,6 +278,7 @@ struct ResourceFileFlattenerOptions { bool keep_raw_values = false; bool do_not_compress_anything = false; bool update_proguard_spec = false; + bool do_not_fail_on_missing_resources = false; OutputFormat output_format = OutputFormat::kApk; std::unordered_set<std::string> extensions_to_not_compress; Maybe<std::regex> regex_to_not_compress; @@ -297,6 +307,25 @@ struct R { }; }; +template <typename T> +uint32_t GetCompressionFlags(const StringPiece& str, T options) { + if (options.do_not_compress_anything) { + return 0; + } + + if (options.regex_to_not_compress + && std::regex_search(str.to_string(), options.regex_to_not_compress.value())) { + return 0; + } + + for (const std::string& extension : options.extensions_to_not_compress) { + if (util::EndsWith(str, extension)) { + return 0; + } + } + return ArchiveEntry::kCompress; +} + class ResourceFileFlattener { public: ResourceFileFlattener(const ResourceFileFlattenerOptions& options, IAaptContext* context, @@ -321,8 +350,6 @@ class ResourceFileFlattener { std::string dst_path; }; - uint32_t GetCompressionFlags(const StringPiece& str); - std::vector<std::unique_ptr<xml::XmlResource>> LinkAndVersionXmlFile(ResourceTable* table, FileOperation* file_op); @@ -381,26 +408,6 @@ ResourceFileFlattener::ResourceFileFlattener(const ResourceFileFlattenerOptions& } } -// TODO(rtmitchell): turn this function into a variable that points to a method that retrieves the -// compression flag -uint32_t ResourceFileFlattener::GetCompressionFlags(const StringPiece& str) { - if (options_.do_not_compress_anything) { - return 0; - } - - if (options_.regex_to_not_compress - && std::regex_search(str.to_string(), options_.regex_to_not_compress.value())) { - return 0; - } - - for (const std::string& extension : options_.extensions_to_not_compress) { - if (util::EndsWith(str, extension)) { - return 0; - } - } - return ArchiveEntry::kCompress; -} - static bool IsTransitionElement(const std::string& name) { return name == "fade" || name == "changeBounds" || name == "slide" || name == "explode" || name == "changeImageTransform" || name == "changeTransform" || @@ -438,7 +445,7 @@ std::vector<std::unique_ptr<xml::XmlResource>> ResourceFileFlattener::LinkAndVer xml::StripAndroidStudioAttributes(doc->root.get()); XmlReferenceLinker xml_linker; - if (!xml_linker.Consume(context_, doc)) { + if (!options_.do_not_fail_on_missing_resources && !xml_linker.Consume(context_, doc)) { return {}; } @@ -640,7 +647,8 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv } } else { error |= !io::CopyFileToArchive(context_, file_op.file_to_copy, file_op.dst_path, - GetCompressionFlags(file_op.dst_path), archive_writer); + GetCompressionFlags(file_op.dst_path, options_), + archive_writer); } } } @@ -887,7 +895,7 @@ class Linker { // android:versionCode from the framework AndroidManifest.xml. ExtractCompileSdkVersions(asset_source->GetAssetManager()); } - } else if (asset_source->IsPackageDynamic(entry.first)) { + } else if (asset_source->IsPackageDynamic(entry.first, entry.second)) { final_table_.included_packages_[entry.first] = entry.second; } } @@ -965,6 +973,17 @@ class Linker { app_info.min_sdk_version = ResourceUtils::ParseSdkVersion(min_sdk->value); } } + + for (const xml::Element* child_el : manifest_el->GetChildElements()) { + if (child_el->namespace_uri.empty() && child_el->name == "uses-split") { + if (const xml::Attribute* split_name = + child_el->FindAttribute(xml::kSchemaAndroid, "name")) { + if (!split_name->value.empty()) { + app_info.split_name_dependencies.insert(split_name->value); + } + } + } + } return app_info; } @@ -1065,7 +1084,8 @@ class Linker { case OutputFormat::kProto: { pb::ResourceTable pb_table; - SerializeTableToPb(*table, &pb_table, context_->GetDiagnostics()); + SerializeTableToPb(*table, &pb_table, context_->GetDiagnostics(), + options_.proto_table_flattener_options); return io::CopyProtoToArchive(context_, &pb_table, kProtoResourceTablePath, ArchiveEntry::kCompress, writer); } break; @@ -1278,7 +1298,8 @@ class Linker { return false; } - proguard::WriteKeepSet(keep_set, &fout, options_.generate_minimal_proguard_rules); + proguard::WriteKeepSet(keep_set, &fout, options_.generate_minimal_proguard_rules, + options_.no_proguard_location_reference); fout.Flush(); if (fout.HadError()) { @@ -1548,16 +1569,7 @@ class Linker { } for (auto& entry : merged_assets) { - uint32_t compression_flags = ArchiveEntry::kCompress; - std::string extension = file::GetExtension(entry.first).to_string(); - - if (options_.do_not_compress_anything - || options_.extensions_to_not_compress.count(extension) > 0 - || (options_.regex_to_not_compress - && std::regex_search(extension, options_.regex_to_not_compress.value()))) { - compression_flags = 0u; - } - + uint32_t compression_flags = GetCompressionFlags(entry.first, options_); if (!io::CopyFileToArchive(context_, entry.second.get(), entry.first, compression_flags, writer)) { return false; @@ -1566,6 +1578,93 @@ class Linker { return true; } + void AliasAdaptiveIcon(xml::XmlResource* manifest, ResourceTable* table) { + xml::Element* application = manifest->root->FindChild("", "application"); + if (!application) { + return; + } + + xml::Attribute* icon = application->FindAttribute(xml::kSchemaAndroid, "icon"); + xml::Attribute* round_icon = application->FindAttribute(xml::kSchemaAndroid, "roundIcon"); + if (!icon || !round_icon) { + return; + } + + // Find the icon resource defined within the application. + auto icon_reference = ValueCast<Reference>(icon->compiled_value.get()); + if (!icon_reference || !icon_reference->name) { + return; + } + auto package = table->FindPackageById(icon_reference->id.value().package_id()); + if (!package) { + return; + } + auto type = package->FindType(icon_reference->name.value().type); + if (!type) { + return; + } + auto icon_entry = type->FindEntry(icon_reference->name.value().entry); + if (!icon_entry) { + return; + } + + int icon_max_sdk = 0; + for (auto& config_value : icon_entry->values) { + icon_max_sdk = (icon_max_sdk < config_value->config.sdkVersion) + ? config_value->config.sdkVersion : icon_max_sdk; + } + if (icon_max_sdk < SDK_O) { + // Adaptive icons must be versioned with v26 qualifiers, so this is not an adaptive icon. + return; + } + + // Find the roundIcon resource defined within the application. + auto round_icon_reference = ValueCast<Reference>(round_icon->compiled_value.get()); + if (!round_icon_reference || !round_icon_reference->name) { + return; + } + package = table->FindPackageById(round_icon_reference->id.value().package_id()); + if (!package) { + return; + } + type = package->FindType(round_icon_reference->name.value().type); + if (!type) { + return; + } + auto round_icon_entry = type->FindEntry(round_icon_reference->name.value().entry); + if (!round_icon_entry) { + return; + } + + int round_icon_max_sdk = 0; + for (auto& config_value : round_icon_entry->values) { + round_icon_max_sdk = (round_icon_max_sdk < config_value->config.sdkVersion) + ? config_value->config.sdkVersion : round_icon_max_sdk; + } + if (round_icon_max_sdk >= SDK_O) { + // The developer explicitly used a v26 compatible drawable as the roundIcon, meaning we should + // not generate an alias to the icon drawable. + return; + } + + // Add an equivalent v26 entry to the roundIcon for each v26 variant of the regular icon. + for (auto& config_value : icon_entry->values) { + if (config_value->config.sdkVersion < SDK_O) { + continue; + } + + context_->GetDiagnostics()->Note(DiagMessage() << "generating " + << round_icon_reference->name.value() + << " with config \"" << config_value->config + << "\" for round icon compatibility"); + + auto value = icon_reference->Clone(&table->string_pool); + auto round_config_value = round_icon_entry->FindOrCreateValue( + config_value->config, config_value->product); + round_config_value->value.reset(value); + } + } + // Writes the AndroidManifest, ResourceTable, and all XML files referenced by the ResourceTable // to the IArchiveWriter. bool WriteApk(IArchiveWriter* writer, proguard::KeepSet* keep_set, xml::XmlResource* manifest, @@ -1579,6 +1678,14 @@ class Linker { return false; } + // When a developer specifies an adaptive application icon, and a non-adaptive round application + // icon, create an alias from the round icon to the regular icon for v26 APIs and up. We do this + // because certain devices prefer android:roundIcon over android:icon regardless of the API + // levels of the drawables set for either. This auto-aliasing behaviour allows an app to prefer + // the android:roundIcon on API 25 devices, and prefer the adaptive icon on API 26 devices. + // See (b/34829129) + AliasAdaptiveIcon(manifest, table); + ResourceFileFlattenerOptions file_flattener_options; file_flattener_options.keep_raw_values = keep_raw_values; file_flattener_options.do_not_compress_anything = options_.do_not_compress_anything; @@ -1591,9 +1698,9 @@ class Linker { file_flattener_options.update_proguard_spec = static_cast<bool>(options_.generate_proguard_rules_path); file_flattener_options.output_format = options_.output_format; + file_flattener_options.do_not_fail_on_missing_resources = options_.merge_only; ResourceFileFlattener file_flattener(file_flattener_options, context_, keep_set); - if (!file_flattener.Flatten(table, writer)) { context_->GetDiagnostics()->Error(DiagMessage() << "failed linking file resources"); return false; @@ -1660,10 +1767,9 @@ class Linker { return 1; } - // First extract the package name without modifying it (via --rename-manifest-package). + // First extract the Package name without modifying it (via --rename-manifest-package). if (Maybe<AppInfo> maybe_app_info = ExtractAppInfoFromManifest(manifest_xml.get(), context_->GetDiagnostics())) { - // Extract the package name from the manifest ignoring the value of --rename-manifest-package. const AppInfo& app_info = maybe_app_info.value(); context_->SetCompilationPackage(app_info.package); } @@ -1699,6 +1805,7 @@ class Linker { context_->SetMinSdkVersion(app_info_.min_sdk_version.value_or_default(0)); context_->SetNameManglerPolicy(NameManglerPolicy{context_->GetCompilationPackage()}); + context_->SetSplitNameDependencies(app_info_.split_name_dependencies); // Override the package ID when it is "android". if (context_->GetCompilationPackage() == "android") { @@ -1714,6 +1821,8 @@ class Linker { TableMergerOptions table_merger_options; table_merger_options.auto_add_overlay = options_.auto_add_overlay; + table_merger_options.override_styles_instead_of_overlaying = + options_.override_styles_instead_of_overlaying; table_merger_options.strict_visibility = options_.strict_visibility; table_merger_ = util::make_unique<TableMerger>(context_, &final_table_, table_merger_options); @@ -1828,7 +1937,7 @@ class Linker { } ReferenceLinker linker; - if (!linker.Consume(context_, &final_table_)) { + if (!options_.merge_only && !linker.Consume(context_, &final_table_)) { context_->GetDiagnostics()->Error(DiagMessage() << "failed linking references"); return 1; } @@ -1980,7 +2089,7 @@ class Linker { manifest_xml->file.name.package = context_->GetCompilationPackage(); XmlReferenceLinker manifest_linker; - if (manifest_linker.Consume(context_, manifest_xml.get())) { + if (options_.merge_only || manifest_linker.Consume(context_, manifest_xml.get())) { if (options_.generate_proguard_rules_path && !proguard::CollectProguardRulesForManifest(manifest_xml.get(), &proguard_keep_set)) { error = true; @@ -2114,6 +2223,12 @@ int LinkCommand::Action(const std::vector<std::string>& args) { return 1; } + if (options_.merge_only && !static_lib_) { + context.GetDiagnostics()->Error( + DiagMessage() << "the --merge-only flag can be only used when building a static library"); + return 1; + } + // The default build type. context.SetPackageType(PackageType::kApp); context.SetPackageId(kAppPackageId); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index e62e0a6b9f62..852b1244cd6e 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -24,6 +24,7 @@ #include "Resource.h" #include "split/TableSplitter.h" #include "format/binary/TableFlattener.h" +#include "format/proto/ProtoSerialize.h" #include "link/ManifestFixer.h" #include "trace/TraceBuffer.h" @@ -42,6 +43,7 @@ struct LinkOptions { std::vector<std::string> assets_dirs; bool output_to_directory = false; bool auto_add_overlay = false; + bool override_styles_instead_of_overlaying = false; OutputFormat output_format = OutputFormat::kApk; Maybe<std::string> rename_resources_package; @@ -55,6 +57,7 @@ struct LinkOptions { bool generate_conditional_proguard_rules = false; bool generate_minimal_proguard_rules = false; bool generate_non_final_ids = false; + bool no_proguard_location_reference = false; std::vector<std::string> javadoc_annotations; Maybe<std::string> private_symbols; @@ -71,6 +74,7 @@ struct LinkOptions { // Static lib options. bool no_static_lib_packages = false; + bool merge_only = false; // AndroidManifest.xml massaging options. ManifestFixerOptions manifest_fixer_options; @@ -80,6 +84,7 @@ struct LinkOptions { // Flattening options. TableFlattenerOptions table_flattener_options; + SerializeTableOptions proto_table_flattener_options; bool keep_raw_values = false; // Split APK options. @@ -212,6 +217,9 @@ class LinkCommand : public Command { "Generates R.java without the final modifier. This is implied when\n" "--static-lib is specified.", &options_.generate_non_final_ids); + AddOptionalSwitch("--no-proguard-location-reference", + "Keep proguard rules files from having a reference to the source file", + &options_.no_proguard_location_reference); AddOptionalFlag("--stable-ids", "File containing a list of name to ID mapping.", &stable_id_file_path_); AddOptionalFlag("--emit-ids", @@ -243,6 +251,10 @@ class LinkCommand : public Command { "Allows the addition of new resources in overlays without\n" "<add-resource> tags.", &options_.auto_add_overlay); + AddOptionalSwitch("--override-styles-instead-of-overlaying", + "Causes styles defined in -R resources to replace previous definitions\n" + "instead of merging into them\n", + &options_.override_styles_instead_of_overlaying); AddOptionalFlag("--rename-manifest-package", "Renames the package in AndroidManifest.xml.", &options_.manifest_fixer_options.rename_manifest_package); AddOptionalFlag("--rename-resources-package", "Renames the package in resources table", @@ -255,7 +267,7 @@ class LinkCommand : public Command { "Changes the name of the target package for overlay. Most useful\n" "when used in conjunction with --rename-manifest-package.", &options_.manifest_fixer_options.rename_overlay_target_package); - AddOptionalFlagList("-0", "File extensions not to compress.", + AddOptionalFlagList("-0", "File suffix not to compress.", &options_.extensions_to_not_compress); AddOptionalSwitch("--no-compress", "Do not compress any resources.", &options_.do_not_compress_anything); @@ -263,8 +275,8 @@ class LinkCommand : public Command { &options_.keep_raw_values); AddOptionalFlag("--no-compress-regex", "Do not compress extensions matching the regular expression. Remember to\n" - " use the '$' symbol for end of line. Uses a non case-sensitive\n" - " ECMAScript regular expression grammar.", + "use the '$' symbol for end of line. Uses a case-sensitive ECMAScript" + "regular expression grammar.", &no_compress_regex); AddOptionalSwitch("--warn-manifest-validation", "Treat manifest validation errors as warnings.", @@ -284,9 +296,18 @@ class LinkCommand : public Command { AddOptionalSwitch("--strict-visibility", "Do not allow overlays with different visibility levels.", &options_.strict_visibility); + AddOptionalSwitch("--exclude-sources", + "Do not serialize source file information when generating resources in\n" + "Protobuf format.", + &options_.proto_table_flattener_options.exclude_sources); + AddOptionalFlag("--trace-folder", + "Generate systrace json trace fragment to specified folder.", + &trace_folder_); + AddOptionalSwitch("--merge-only", + "Only merge the resources, without verifying resource references. This flag\n" + "should only be used together with the --static-lib flag.", + &options_.merge_only); AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_); - AddOptionalFlag("--trace-folder", "Generate systrace json trace fragment to specified folder.", - &trace_folder_); } int Action(const std::vector<std::string>& args) override; diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index 9ea93f638aff..062dd8eac975 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#include "AppInfo.h" #include "Link.h" #include "LoadedApk.h" @@ -43,10 +44,8 @@ TEST_F(LinkTest, RemoveRawXmlStrings) { // Load the binary xml tree android::ResXMLTree tree; std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); - std::unique_ptr<io::IData> data = OpenFileAsData(apk.get(), "res/xml/test.xml"); ASSERT_THAT(data, Ne(nullptr)); - AssertLoadXml(apk.get(), data.get(), &tree); // Check that the raw string index has not been assigned @@ -71,10 +70,8 @@ TEST_F(LinkTest, KeepRawXmlStrings) { // Load the binary xml tree android::ResXMLTree tree; std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); - std::unique_ptr<io::IData> data = OpenFileAsData(apk.get(), "res/xml/test.xml"); ASSERT_THAT(data, Ne(nullptr)); - AssertLoadXml(apk.get(), data.get(), &tree); // Check that the raw string index has been set to the correct string pool entry @@ -83,4 +80,241 @@ TEST_F(LinkTest, KeepRawXmlStrings) { EXPECT_THAT(util::GetString(tree.getStrings(), static_cast<size_t>(raw_index)), Eq("007")); } -} // namespace aapt
\ No newline at end of file +TEST_F(LinkTest, NoCompressAssets) { + StdErrDiagnostics diag; + std::string content(500, 'a'); + WriteFile(GetTestPath("assets/testtxt"), content); + WriteFile(GetTestPath("assets/testtxt2"), content); + WriteFile(GetTestPath("assets/test.txt"), content); + WriteFile(GetTestPath("assets/test.hello.txt"), content); + WriteFile(GetTestPath("assets/test.hello.xml"), content); + + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", GetDefaultManifest(), + "-o", out_apk, + "-0", ".txt", + "-0", "txt2", + "-0", ".hello.txt", + "-0", "hello.xml", + "-A", GetTestPath("assets") + }; + + ASSERT_TRUE(Link(link_args, &diag)); + + std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); + ASSERT_THAT(apk, Ne(nullptr)); + io::IFileCollection* zip = apk->GetFileCollection(); + ASSERT_THAT(zip, Ne(nullptr)); + + auto file = zip->FindFile("assets/testtxt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_TRUE(file->WasCompressed()); + + file = zip->FindFile("assets/testtxt2"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("assets/test.txt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("assets/test.hello.txt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("assets/test.hello.xml"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); +} + +TEST_F(LinkTest, NoCompressResources) { + StdErrDiagnostics diag; + std::string content(500, 'a'); + const std::string compiled_files_dir = GetTestPath("compiled"); + ASSERT_TRUE(CompileFile(GetTestPath("res/raw/testtxt"), content, compiled_files_dir, &diag)); + ASSERT_TRUE(CompileFile(GetTestPath("res/raw/test.txt"), content, compiled_files_dir, &diag)); + ASSERT_TRUE(CompileFile(GetTestPath("res/raw/test1.hello.txt"), content, compiled_files_dir, + &diag)); + ASSERT_TRUE(CompileFile(GetTestPath("res/raw/test2.goodbye.xml"), content, compiled_files_dir, + &diag)); + + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", GetDefaultManifest(), + "-o", out_apk, + "-0", ".txt", + "-0", ".hello.txt", + "-0", "goodbye.xml", + }; + + ASSERT_TRUE(Link(link_args, compiled_files_dir, &diag)); + + std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); + ASSERT_THAT(apk, Ne(nullptr)); + io::IFileCollection* zip = apk->GetFileCollection(); + ASSERT_THAT(zip, Ne(nullptr)); + + auto file = zip->FindFile("res/raw/testtxt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_TRUE(file->WasCompressed()); + + file = zip->FindFile("res/raw/test.txt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("res/raw/test1.hello.hello.txt"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); + + file = zip->FindFile("res/raw/test2.goodbye.goodbye.xml"); + ASSERT_THAT(file, Ne(nullptr)); + EXPECT_FALSE(file->WasCompressed()); +} + +TEST_F(LinkTest, OverlayStyles) { + StdErrDiagnostics diag; + const std::string compiled_files_dir = GetTestPath("compiled"); + const std::string override_files_dir = GetTestPath("compiled-override"); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"), + R"(<resources> + <style name="MyStyle"> + <item name="android:textColor">#123</item> + </style> + </resources>)", + compiled_files_dir, &diag)); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values-override.xml"), + R"(<resources> + <style name="MyStyle"> + <item name="android:background">#456</item> + </style> + </resources>)", + override_files_dir, &diag)); + + + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", GetDefaultManifest(kDefaultPackageName), + "-o", out_apk, + }; + const auto override_files = file::FindFiles(override_files_dir, &diag); + for (const auto &override_file : override_files.value()) { + link_args.push_back("-R"); + link_args.push_back(file::BuildPath({override_files_dir, override_file})); + } + ASSERT_TRUE(Link(link_args, compiled_files_dir, &diag)); + + std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); + const Style* actual_style = test::GetValue<Style>( + apk->GetResourceTable(), std::string(kDefaultPackageName) + ":style/MyStyle"); + ASSERT_NE(actual_style, nullptr); + ASSERT_EQ(actual_style->entries.size(), 2); + EXPECT_EQ(actual_style->entries[0].key.id, 0x01010098); // android:textColor + EXPECT_EQ(actual_style->entries[1].key.id, 0x010100d4); // android:background +} + +TEST_F(LinkTest, OverrideStylesInsteadOfOverlaying) { + StdErrDiagnostics diag; + const std::string compiled_files_dir = GetTestPath("compiled"); + const std::string override_files_dir = GetTestPath("compiled-override"); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"), + R"(<resources> + <style name="MyStyle"> + <item name="android:textColor">#123</item> + </style> + </resources>)", + compiled_files_dir, &diag)); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values-override.xml"), + R"(<resources> + <style name="MyStyle"> + <item name="android:background">#456</item> + </style> + </resources>)", + override_files_dir, &diag)); + + + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", GetDefaultManifest(kDefaultPackageName), + "--override-styles-instead-of-overlaying", + "-o", out_apk, + }; + const auto override_files = file::FindFiles(override_files_dir, &diag); + for (const auto &override_file : override_files.value()) { + link_args.push_back("-R"); + link_args.push_back(file::BuildPath({override_files_dir, override_file})); + } + ASSERT_TRUE(Link(link_args, compiled_files_dir, &diag)); + + std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(out_apk, &diag); + const Style* actual_style = test::GetValue<Style>( + apk->GetResourceTable(), std::string(kDefaultPackageName) + ":style/MyStyle"); + ASSERT_NE(actual_style, nullptr); + ASSERT_EQ(actual_style->entries.size(), 1); + EXPECT_EQ(actual_style->entries[0].key.id, 0x010100d4); // android:background +} + +TEST_F(LinkTest, AppInfoWithUsesSplit) { + StdErrDiagnostics diag; + const std::string base_files_dir = GetTestPath("base"); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"), + R"(<resources> + <string name="bar">bar</string> + </resources>)", + base_files_dir, &diag)); + const std::string base_apk = GetTestPath("base.apk"); + std::vector<std::string> link_args = { + "--manifest", GetDefaultManifest("com.aapt2.app"), + "-o", base_apk, + }; + ASSERT_TRUE(Link(link_args, base_files_dir, &diag)); + + const std::string feature_manifest = GetTestPath("feature_manifest.xml"); + WriteFile(feature_manifest, android::base::StringPrintf(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.aapt2.app" split="feature1"> + </manifest>)")); + const std::string feature_files_dir = GetTestPath("feature"); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"), + R"(<resources> + <string name="foo">foo</string> + </resources>)", + feature_files_dir, &diag)); + const std::string feature_apk = GetTestPath("feature.apk"); + const std::string feature_package_id = "0x80"; + link_args = { + "--manifest", feature_manifest, + "-I", base_apk, + "--package-id", feature_package_id, + "-o", feature_apk, + }; + ASSERT_TRUE(Link(link_args, feature_files_dir, &diag)); + + const std::string feature2_manifest = GetTestPath("feature2_manifest.xml"); + WriteFile(feature2_manifest, android::base::StringPrintf(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.aapt2.app" split="feature2"> + <uses-split android:name="feature1"/> + </manifest>)")); + const std::string feature2_files_dir = GetTestPath("feature2"); + ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"), + R"(<resources> + <string-array name="string_array"> + <item>@string/bar</item> + <item>@string/foo</item> + </string-array> + </resources>)", + feature2_files_dir, &diag)); + const std::string feature2_apk = GetTestPath("feature2.apk"); + const std::string feature2_package_id = "0x81"; + link_args = { + "--manifest", feature2_manifest, + "-I", base_apk, + "-I", feature_apk, + "--package-id", feature2_package_id, + "-o", feature2_apk, + }; + ASSERT_TRUE(Link(link_args, feature2_files_dir, &diag)); +} + +} // namespace aapt diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 2e6af18c1948..e36668e5a043 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -53,9 +53,9 @@ using ::android::ConfigDescription; using ::android::ResTable_config; using ::android::StringPiece; using ::android::base::ReadFileToString; -using ::android::base::WriteStringToFile; using ::android::base::StringAppendF; using ::android::base::StringPrintf; +using ::android::base::WriteStringToFile; namespace aapt { @@ -108,6 +108,12 @@ class OptimizeContext : public IAaptContext { return sdk_version_; } + const std::set<std::string>& GetSplitNameDependencies() override { + UNIMPLEMENTED(FATAL) << "Split Name Dependencies should not be necessary"; + static std::set<std::string> empty; + return empty; + } + private: DISALLOW_COPY_AND_ASSIGN(OptimizeContext); @@ -294,29 +300,7 @@ class Optimizer { OptimizeContext* context_; }; -bool ExtractObfuscationWhitelistFromConfig(const std::string& path, OptimizeContext* context, - OptimizeOptions* options) { - std::string contents; - if (!ReadFileToString(path, &contents, true)) { - context->GetDiagnostics()->Error(DiagMessage() - << "failed to parse whitelist from config file: " << path); - return false; - } - for (StringPiece resource_name : util::Tokenize(contents, ',')) { - options->table_flattener_options.whitelisted_resources.insert( - resource_name.to_string()); - } - return true; -} - -bool ExtractConfig(const std::string& path, OptimizeContext* context, - OptimizeOptions* options) { - std::string content; - if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) { - context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading whitelist"); - return false; - } - +bool ParseConfig(const std::string& content, IAaptContext* context, OptimizeOptions* options) { size_t line_no = 0; for (StringPiece line : util::Tokenize(content, '\n')) { line_no++; @@ -345,15 +329,24 @@ bool ExtractConfig(const std::string& path, OptimizeContext* context, for (StringPiece directive : util::Tokenize(directives, ',')) { if (directive == "remove") { options->resources_blacklist.insert(resource_name.ToResourceName()); - } else if (directive == "no_obfuscate") { - options->table_flattener_options.whitelisted_resources.insert( - resource_name.entry.to_string()); + } else if (directive == "no_collapse" || directive == "no_obfuscate") { + options->table_flattener_options.name_collapse_exemptions.insert( + resource_name.ToResourceName()); } } } return true; } +bool ExtractConfig(const std::string& path, IAaptContext* context, OptimizeOptions* options) { + std::string content; + if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) { + context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading config file"); + return false; + } + return ParseConfig(content, context, options); +} + bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk, OptimizeOptions* out_options) { const xml::XmlResource* manifest = apk->GetManifest(); @@ -461,15 +454,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { } } - if (options_.table_flattener_options.collapse_key_stringpool) { - if (whitelist_path_) { - std::string& path = whitelist_path_.value(); - if (!ExtractObfuscationWhitelistFromConfig(path, &context, &options_)) { - return 1; - } - } - } - if (resources_config_path_) { std::string& path = resources_config_path_.value(); if (!ExtractConfig(path, &context, &options_)) { diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 7f4a3ed85364..5070ccc8afbf 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -57,7 +57,7 @@ struct OptimizeOptions { std::unordered_set<std::string> kept_artifacts; // Whether or not to shorten resource paths in the APK. - bool shorten_resource_paths; + bool shorten_resource_paths = false; // Path to the output map of original resource paths to shortened paths. Maybe<std::string> shortened_paths_map_path; @@ -78,10 +78,6 @@ class OptimizeCommand : public Command { "All the resources that would be unused on devices of the given densities will be \n" "removed from the APK.", &target_densities_); - AddOptionalFlag("--whitelist-path", - "Path to the whitelist.cfg file containing whitelisted resources \n" - "whose names should not be altered in final resource tables.", - &whitelist_path_); AddOptionalFlag("--resources-config-path", "Path to the resources.cfg file containing the list of resources and \n" "directives to each resource. \n" @@ -104,11 +100,13 @@ class OptimizeCommand : public Command { "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.", &options_.table_flattener_options.use_sparse_entries); - AddOptionalSwitch("--enable-resource-obfuscation", - "Enables obfuscation of key string pool to single value", + AddOptionalSwitch("--collapse-resource-names", + "Collapses resource names to a single value in the key string pool. Resources can \n" + "be exempted using the \"no_collapse\" directive in a file specified by " + "--resources-config-path.", &options_.table_flattener_options.collapse_key_stringpool); - AddOptionalSwitch("--enable-resource-path-shortening", - "Enables shortening of the path of the resources inside the APK.", + AddOptionalSwitch("--shorten-resource-paths", + "Shortens the paths of resources inside the APK.", &options_.shorten_resource_paths); AddOptionalFlag("--resource-path-shortening-map", "Path to output the map of old resource paths to shortened paths.", @@ -125,7 +123,6 @@ class OptimizeCommand : public Command { const std::string &file_path); Maybe<std::string> config_path_; - Maybe<std::string> whitelist_path_; Maybe<std::string> resources_config_path_; Maybe<std::string> target_densities_; std::vector<std::string> configs_; diff --git a/tools/aapt2/cmd/Optimize_test.cpp b/tools/aapt2/cmd/Optimize_test.cpp new file mode 100644 index 000000000000..ac681e85b3d6 --- /dev/null +++ b/tools/aapt2/cmd/Optimize_test.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Optimize.h" + +#include "AppInfo.h" +#include "Diagnostics.h" +#include "LoadedApk.h" +#include "Resource.h" +#include "test/Test.h" + +using testing::Contains; +using testing::Eq; + +namespace aapt { + +bool ParseConfig(const std::string&, IAaptContext*, OptimizeOptions*); + +using OptimizeTest = CommandTestFixture; + +TEST_F(OptimizeTest, ParseConfigWithNoCollapseExemptions) { + const std::string& content = R"( +string/foo#no_collapse +dimen/bar#no_collapse +)"; + aapt::test::Context context; + OptimizeOptions options; + ParseConfig(content, &context, &options); + + const std::set<ResourceName>& name_collapse_exemptions = + options.table_flattener_options.name_collapse_exemptions; + + ASSERT_THAT(name_collapse_exemptions.size(), Eq(2)); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo"))); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar"))); +} + +TEST_F(OptimizeTest, ParseConfigWithNoObfuscateExemptions) { + const std::string& content = R"( +string/foo#no_obfuscate +dimen/bar#no_obfuscate +)"; + aapt::test::Context context; + OptimizeOptions options; + ParseConfig(content, &context, &options); + + const std::set<ResourceName>& name_collapse_exemptions = + options.table_flattener_options.name_collapse_exemptions; + + ASSERT_THAT(name_collapse_exemptions.size(), Eq(2)); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo"))); + EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar"))); +} + +} // namespace aapt diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index e2c65ba74271..7214f1a68d2c 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -436,9 +436,9 @@ void SetLongVersionCode(xml::Element* manifest, uint64_t version) { } std::regex GetRegularExpression(const std::string &input) { - // Standard ECMAScript grammar plus case insensitive. + // Standard ECMAScript grammar. std::regex case_insensitive( - input, std::regex_constants::icase | std::regex_constants::ECMAScript); + input, std::regex_constants::ECMAScript); return case_insensitive; } diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp index 7e492610b658..ac1f981d753c 100644 --- a/tools/aapt2/cmd/Util_test.cpp +++ b/tools/aapt2/cmd/Util_test.cpp @@ -383,7 +383,7 @@ TEST (UtilTest, AdjustSplitConstraintsForMinSdk) { EXPECT_NE(*adjusted_contraints[1].configs.begin(), ConfigDescription::DefaultConfig()); } -TEST(UtilTest, RegularExperssions) { +TEST (UtilTest, RegularExperssionsSimple) { std::string valid(".bc$"); std::regex expression = GetRegularExpression(valid); EXPECT_TRUE(std::regex_search("file.abc", expression)); @@ -391,4 +391,24 @@ TEST(UtilTest, RegularExperssions) { EXPECT_FALSE(std::regex_search("abc.zip", expression)); } +TEST (UtilTest, RegularExpressionComplex) { + std::string valid("\\.(d|D)(e|E)(x|X)$"); + std::regex expression = GetRegularExpression(valid); + EXPECT_TRUE(std::regex_search("file.dex", expression)); + EXPECT_TRUE(std::regex_search("file.DEX", expression)); + EXPECT_TRUE(std::regex_search("file.dEx", expression)); + EXPECT_FALSE(std::regex_search("file.dexx", expression)); + EXPECT_FALSE(std::regex_search("dex.file", expression)); + EXPECT_FALSE(std::regex_search("file.adex", expression)); +} + +TEST (UtilTest, RegularExpressionNonEnglish) { + std::string valid("\\.(k|K)(o|O)(ń|Ń)(c|C)(ó|Ó)(w|W)(k|K)(a|A)$"); + std::regex expression = GetRegularExpression(valid); + EXPECT_TRUE(std::regex_search("file.końcówka", expression)); + EXPECT_TRUE(std::regex_search("file.KOŃCÓWKA", expression)); + EXPECT_TRUE(std::regex_search("file.kOńcÓwkA", expression)); + EXPECT_FALSE(std::regex_search("file.koncowka", expression)); +} + } // namespace aapt diff --git a/tools/aapt2/dump/DumpManifest.cpp b/tools/aapt2/dump/DumpManifest.cpp index 42a64716701d..4a6bfd031284 100644 --- a/tools/aapt2/dump/DumpManifest.cpp +++ b/tools/aapt2/dump/DumpManifest.cpp @@ -91,6 +91,7 @@ enum { }; const std::string& kAndroidNamespace = "http://schemas.android.com/apk/res/android"; +constexpr int kCurrentDevelopmentVersion = 10000; /** Retrieves the attribute of the element with the specified attribute resource id. */ static xml::Attribute* FindAttribute(xml::Element *el, uint32_t resd_id) { @@ -291,7 +292,10 @@ class ManifestExtractor { } } } - return &attr->value; + + if (!attr->value.empty()) { + return &attr->value; + } } return nullptr; } @@ -322,7 +326,7 @@ class ManifestExtractor { ConfigDescription config; config.orientation = android::ResTable_config::ORIENTATION_PORT; config.density = android::ResTable_config::DENSITY_MEDIUM; - config.sdkVersion = 10000; // Very high. + config.sdkVersion = kCurrentDevelopmentVersion; // Very high. config.screenWidthDp = 320; config.screenHeightDp = 480; config.smallestScreenWidthDp = 320; @@ -425,6 +429,8 @@ class Manifest : public ManifestExtractor::Element { const std::string* split = nullptr; const std::string* platformVersionName = nullptr; const std::string* platformVersionCode = nullptr; + const int32_t* platformVersionNameInt = nullptr; + const int32_t* platformVersionCodeInt = nullptr; const int32_t* compilesdkVersion = nullptr; const std::string* compilesdkVersionCodename = nullptr; const int32_t* installLocation = nullptr; @@ -440,6 +446,10 @@ class Manifest : public ManifestExtractor::Element { "platformBuildVersionName")); platformVersionCode = GetAttributeString(FindAttribute(manifest, {}, "platformBuildVersionCode")); + platformVersionNameInt = GetAttributeInteger(FindAttribute(manifest, {}, + "platformBuildVersionName")); + platformVersionCodeInt = GetAttributeInteger(FindAttribute(manifest, {}, + "platformBuildVersionCode")); // Extract the compile sdk info compilesdkVersion = GetAttributeInteger(FindAttribute(manifest, COMPILE_SDK_VERSION_ATTR)); @@ -459,9 +469,13 @@ class Manifest : public ManifestExtractor::Element { } if (platformVersionName) { printer->Print(StringPrintf(" platformBuildVersionName='%s'", platformVersionName->data())); + } else if (platformVersionNameInt) { + printer->Print(StringPrintf(" platformBuildVersionName='%d'", *platformVersionNameInt)); } if (platformVersionCode) { printer->Print(StringPrintf(" platformBuildVersionCode='%s'", platformVersionCode->data())); + } else if (platformVersionCodeInt) { + printer->Print(StringPrintf(" platformBuildVersionCode='%d'", *platformVersionCodeInt)); } if (compilesdkVersion) { printer->Print(StringPrintf(" compileSdkVersion='%d'", *compilesdkVersion)); @@ -609,6 +623,8 @@ class UsesSdkBadging : public ManifestExtractor::Element { } if (target_sdk) { extractor()->RaiseTargetSdk(*target_sdk); + } else if (target_sdk_name) { + extractor()->RaiseTargetSdk(kCurrentDevelopmentVersion); } } diff --git a/tools/aapt2/format/Archive.cpp b/tools/aapt2/format/Archive.cpp index d152a9cc7e62..41f01a01ed7c 100644 --- a/tools/aapt2/format/Archive.cpp +++ b/tools/aapt2/format/Archive.cpp @@ -103,7 +103,13 @@ class DirectoryWriter : public IArchiveWriter { return false; } } - return !in->HadError(); + + if (in->HadError()) { + error_ = in->GetError(); + return false; + } + + return FinishEntry(); } bool HadError() const override { @@ -191,6 +197,7 @@ class ZipFileWriter : public IArchiveWriter { } if (in->HadError()) { + error_ = in->GetError(); return false; } diff --git a/tools/aapt2/format/Archive_test.cpp b/tools/aapt2/format/Archive_test.cpp new file mode 100644 index 000000000000..ceed3740f37a --- /dev/null +++ b/tools/aapt2/format/Archive_test.cpp @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "test/Test.h" + +namespace aapt { + +using ArchiveTest = TestDirectoryFixture; + +constexpr size_t kTestDataLength = 100; + +class TestData : public io::MallocData { + public: + TestData(std::unique_ptr<uint8_t[]>& data, size_t size) + : MallocData(std::move(data), size) {} + + bool HadError() const override { return !error_.empty(); } + + std::string GetError() const override { return error_; } + + std::string error_; +}; + +std::unique_ptr<uint8_t[]> MakeTestArray() { + auto array = std::make_unique<uint8_t[]>(kTestDataLength); + for (int index = 0; index < kTestDataLength; ++index) { + array[index] = static_cast<uint8_t>(rand()); + } + return array; +} + +std::unique_ptr<IArchiveWriter> MakeDirectoryWriter(const std::string& output_path) { + file::mkdirs(output_path); + + StdErrDiagnostics diag; + return CreateDirectoryArchiveWriter(&diag, output_path); +} + +std::unique_ptr<IArchiveWriter> MakeZipFileWriter(const std::string& output_path) { + file::mkdirs(file::GetStem(output_path).to_string()); + std::remove(output_path.c_str()); + + StdErrDiagnostics diag; + return CreateZipFileArchiveWriter(&diag, output_path); +} + +void VerifyDirectory(const std::string& path, const std::string& file, const uint8_t array[]) { + std::string file_path = file::BuildPath({path, file}); + auto buffer = std::make_unique<char[]>(kTestDataLength); + std::ifstream stream(file_path); + stream.read(buffer.get(), kTestDataLength); + + for (int index = 0; index < kTestDataLength; ++index) { + ASSERT_EQ(array[index], static_cast<uint8_t>(buffer[index])); + } +} + +void VerifyZipFile(const std::string& output_path, const std::string& file, const uint8_t array[]) { + std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(output_path, nullptr); + std::unique_ptr<io::InputStream> stream = zip->FindFile(file)->OpenInputStream(); + + std::vector<uint8_t> buffer; + const void* data; + size_t size; + + while (stream->Next(&data, &size)) { + auto pointer = static_cast<const uint8_t*>(data); + buffer.insert(buffer.end(), pointer, pointer + size); + } + + for (int index = 0; index < kTestDataLength; ++index) { + ASSERT_EQ(array[index], buffer[index]); + } +} + +TEST_F(ArchiveTest, DirectoryWriteEntrySuccess) { + std::string output_path = GetTestPath("output"); + std::unique_ptr<IArchiveWriter> writer = MakeDirectoryWriter(output_path); + std::unique_ptr<uint8_t[]> data1 = MakeTestArray(); + std::unique_ptr<uint8_t[]> data2 = MakeTestArray(); + + ASSERT_TRUE(writer->StartEntry("test1", 0)); + ASSERT_TRUE(writer->Write(static_cast<const void*>(data1.get()), kTestDataLength)); + ASSERT_TRUE(writer->FinishEntry()); + ASSERT_FALSE(writer->HadError()); + + ASSERT_TRUE(writer->StartEntry("test2", 0)); + ASSERT_TRUE(writer->Write(static_cast<const void*>(data2.get()), kTestDataLength)); + ASSERT_TRUE(writer->FinishEntry()); + ASSERT_FALSE(writer->HadError()); + + writer.reset(); + + VerifyDirectory(output_path, "test1", data1.get()); + VerifyDirectory(output_path, "test2", data2.get()); +} + +TEST_F(ArchiveTest, DirectoryWriteFileSuccess) { + std::string output_path = GetTestPath("output"); + std::unique_ptr<IArchiveWriter> writer = MakeDirectoryWriter(output_path); + + std::unique_ptr<uint8_t[]> data1 = MakeTestArray(); + auto data1_copy = std::make_unique<uint8_t[]>(kTestDataLength); + std::copy(data1.get(), data1.get() + kTestDataLength, data1_copy.get()); + + std::unique_ptr<uint8_t[]> data2 = MakeTestArray(); + auto data2_copy = std::make_unique<uint8_t[]>(kTestDataLength); + std::copy(data2.get(), data2.get() + kTestDataLength, data2_copy.get()); + + auto input1 = std::make_unique<TestData>(data1_copy, kTestDataLength); + auto input2 = std::make_unique<TestData>(data2_copy, kTestDataLength); + + ASSERT_TRUE(writer->WriteFile("test1", 0, input1.get())); + ASSERT_FALSE(writer->HadError()); + ASSERT_TRUE(writer->WriteFile("test2", 0, input2.get())); + ASSERT_FALSE(writer->HadError()); + + writer.reset(); + + VerifyDirectory(output_path, "test1", data1.get()); + VerifyDirectory(output_path, "test2", data2.get()); +} + +TEST_F(ArchiveTest, DirectoryWriteFileError) { + std::string output_path = GetTestPath("output"); + std::unique_ptr<IArchiveWriter> writer = MakeDirectoryWriter(output_path); + std::unique_ptr<uint8_t[]> data = MakeTestArray(); + auto input = std::make_unique<TestData>(data, kTestDataLength); + input->error_ = "DirectoryWriteFileError"; + + ASSERT_FALSE(writer->WriteFile("test", 0, input.get())); + ASSERT_TRUE(writer->HadError()); + ASSERT_EQ("DirectoryWriteFileError", writer->GetError()); +} + +TEST_F(ArchiveTest, ZipFileWriteEntrySuccess) { + std::string output_path = GetTestPath("output.apk"); + std::unique_ptr<IArchiveWriter> writer = MakeZipFileWriter(output_path); + std::unique_ptr<uint8_t[]> data1 = MakeTestArray(); + std::unique_ptr<uint8_t[]> data2 = MakeTestArray(); + + ASSERT_TRUE(writer->StartEntry("test1", 0)); + ASSERT_TRUE(writer->Write(static_cast<const void*>(data1.get()), kTestDataLength)); + ASSERT_TRUE(writer->FinishEntry()); + ASSERT_FALSE(writer->HadError()); + + ASSERT_TRUE(writer->StartEntry("test2", 0)); + ASSERT_TRUE(writer->Write(static_cast<const void*>(data2.get()), kTestDataLength)); + ASSERT_TRUE(writer->FinishEntry()); + ASSERT_FALSE(writer->HadError()); + + writer.reset(); + + VerifyZipFile(output_path, "test1", data1.get()); + VerifyZipFile(output_path, "test2", data2.get()); +} + +TEST_F(ArchiveTest, ZipFileWriteFileSuccess) { + std::string output_path = GetTestPath("output.apk"); + std::unique_ptr<IArchiveWriter> writer = MakeZipFileWriter(output_path); + + std::unique_ptr<uint8_t[]> data1 = MakeTestArray(); + auto data1_copy = std::make_unique<uint8_t[]>(kTestDataLength); + std::copy(data1.get(), data1.get() + kTestDataLength, data1_copy.get()); + + std::unique_ptr<uint8_t[]> data2 = MakeTestArray(); + auto data2_copy = std::make_unique<uint8_t[]>(kTestDataLength); + std::copy(data2.get(), data2.get() + kTestDataLength, data2_copy.get()); + + auto input1 = std::make_unique<TestData>(data1_copy, kTestDataLength); + auto input2 = std::make_unique<TestData>(data2_copy, kTestDataLength); + + ASSERT_TRUE(writer->WriteFile("test1", 0, input1.get())); + ASSERT_FALSE(writer->HadError()); + ASSERT_TRUE(writer->WriteFile("test2", 0, input2.get())); + ASSERT_FALSE(writer->HadError()); + + writer.reset(); + + VerifyZipFile(output_path, "test1", data1.get()); + VerifyZipFile(output_path, "test2", data2.get()); +} + +TEST_F(ArchiveTest, ZipFileWriteFileError) { + std::string output_path = GetTestPath("output.apk"); + std::unique_ptr<IArchiveWriter> writer = MakeZipFileWriter(output_path); + std::unique_ptr<uint8_t[]> data = MakeTestArray(); + auto input = std::make_unique<TestData>(data, kTestDataLength); + input->error_ = "ZipFileWriteFileError"; + + ASSERT_FALSE(writer->WriteFile("test", 0, input.get())); + ASSERT_TRUE(writer->HadError()); + ASSERT_EQ("ZipFileWriteFileError", writer->GetError()); +} + +} // namespace aapt diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp index fd8e36ebf823..f362744c0942 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -455,35 +455,6 @@ bool BinaryResourceParser::ParseOverlayable(const ResChunk_header* chunk) { const ResTable_overlayable_policy_header* policy_header = ConvertTo<ResTable_overlayable_policy_header>(parser.chunk()); - OverlayableItem::PolicyFlags policies = OverlayableItem::Policy::kNone; - if (policy_header->policy_flags & ResTable_overlayable_policy_header::POLICY_PUBLIC) { - policies |= OverlayableItem::Policy::kPublic; - } - if (policy_header->policy_flags - & ResTable_overlayable_policy_header::POLICY_SYSTEM_PARTITION) { - policies |= OverlayableItem::Policy::kSystem; - } - if (policy_header->policy_flags - & ResTable_overlayable_policy_header::POLICY_VENDOR_PARTITION) { - policies |= OverlayableItem::Policy::kVendor; - } - if (policy_header->policy_flags - & ResTable_overlayable_policy_header::POLICY_PRODUCT_PARTITION) { - policies |= OverlayableItem::Policy::kProduct; - } - if (policy_header->policy_flags - & ResTable_overlayable_policy_header::POLICY_SIGNATURE) { - policies |= OverlayableItem::Policy::kSignature; - } - if (policy_header->policy_flags - & ResTable_overlayable_policy_header::POLICY_ODM_PARTITION) { - policies |= OverlayableItem::Policy::kOdm; - } - if (policy_header->policy_flags - & ResTable_overlayable_policy_header::POLICY_OEM_PARTITION) { - policies |= OverlayableItem::Policy::kOem; - } - const ResTable_ref* const ref_begin = reinterpret_cast<const ResTable_ref*>( ((uint8_t *)policy_header) + util::DeviceToHost32(policy_header->header.headerSize)); const ResTable_ref* const ref_end = ref_begin @@ -501,7 +472,7 @@ bool BinaryResourceParser::ParseOverlayable(const ResChunk_header* chunk) { } OverlayableItem overlayable_item(overlayable); - overlayable_item.policies = policies; + overlayable_item.policies = policy_header->policy_flags; if (!table_->SetOverlayable(iter->second, overlayable_item, diag_)) { return false; } @@ -614,6 +585,7 @@ std::unique_ptr<Attribute> BinaryResourceParser::ParseAttr(const ResourceNameRef if (attr->type_mask & (ResTable_map::TYPE_ENUM | ResTable_map::TYPE_FLAGS)) { Attribute::Symbol symbol; symbol.value = util::DeviceToHost32(map_entry.value.data); + symbol.type = map_entry.value.dataType; symbol.symbol = Reference(util::DeviceToHost32(map_entry.name.ident)); attr->symbols.push_back(std::move(symbol)); } diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index f2e72da4056a..4784ecf3d12c 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -59,10 +59,22 @@ static void strcpy16_htod(uint16_t* dst, size_t len, const StringPiece16& src) { dst[i] = 0; } +static bool cmp_style_ids(ResourceId a, ResourceId b) { + // If one of a and b is from the framework package (package ID 0x01), and the + // other is a dynamic ID (package ID 0x00), then put the dynamic ID after the + // framework ID. This ensures that when AssetManager resolves the dynamic IDs, + // they will be in sorted order as expected by AssetManager. + if ((a.package_id() == kFrameworkPackageId && b.package_id() == 0x00) || + (a.package_id() == 0x00 && b.package_id() == kFrameworkPackageId)) { + return b < a; + } + return a < b; +} + static bool cmp_style_entries(const Style::Entry& a, const Style::Entry& b) { if (a.key.id) { if (b.key.id) { - return a.key.id.value() < b.key.id.value(); + return cmp_style_ids(a.key.id.value(), b.key.id.value()); } return true; } else if (!b.key.id) { @@ -107,7 +119,7 @@ class MapFlattenVisitor : public ValueVisitor { } for (Attribute::Symbol& s : attr->symbols) { - BinaryPrimitive val(Res_value::TYPE_INT_DEC, s.value); + BinaryPrimitive val(s.type, s.value); FlattenEntry(&s.symbol, &val); } } @@ -221,21 +233,22 @@ class MapFlattenVisitor : public ValueVisitor { struct OverlayableChunk { std::string actor; Source source; - std::map<OverlayableItem::PolicyFlags, std::set<ResourceId>> policy_ids; + std::map<PolicyFlags, std::set<ResourceId>> policy_ids; }; class PackageFlattener { public: PackageFlattener(IAaptContext* context, ResourceTablePackage* package, const std::map<size_t, std::string>* shared_libs, bool use_sparse_entries, - bool collapse_key_stringpool, const std::set<std::string>& whitelisted_resources) + bool collapse_key_stringpool, + const std::set<ResourceName>& name_collapse_exemptions) : context_(context), diag_(context->GetDiagnostics()), package_(package), shared_libs_(shared_libs), use_sparse_entries_(use_sparse_entries), collapse_key_stringpool_(collapse_key_stringpool), - whitelisted_resources_(whitelisted_resources) { + name_collapse_exemptions_(name_collapse_exemptions) { } bool FlattenPackage(BigBuffer* buffer) { @@ -480,35 +493,12 @@ class PackageFlattener { return false; } - uint32_t policy_flags = 0; - if (item.policies & OverlayableItem::Policy::kPublic) { - policy_flags |= ResTable_overlayable_policy_header::POLICY_PUBLIC; - } - if (item.policies & OverlayableItem::Policy::kSystem) { - policy_flags |= ResTable_overlayable_policy_header::POLICY_SYSTEM_PARTITION; - } - if (item.policies & OverlayableItem::Policy::kVendor) { - policy_flags |= ResTable_overlayable_policy_header::POLICY_VENDOR_PARTITION; - } - if (item.policies & OverlayableItem::Policy::kProduct) { - policy_flags |= ResTable_overlayable_policy_header::POLICY_PRODUCT_PARTITION; - } - if (item.policies & OverlayableItem::Policy::kSignature) { - policy_flags |= ResTable_overlayable_policy_header::POLICY_SIGNATURE; - } - if (item.policies & OverlayableItem::Policy::kOdm) { - policy_flags |= ResTable_overlayable_policy_header::POLICY_ODM_PARTITION; - } - if (item.policies & OverlayableItem::Policy::kOem) { - policy_flags |= ResTable_overlayable_policy_header::POLICY_OEM_PARTITION; - } - - auto policy = overlayable_chunk->policy_ids.find(policy_flags); + auto policy = overlayable_chunk->policy_ids.find(item.policies); if (policy != overlayable_chunk->policy_ids.end()) { policy->second.insert(id); } else { overlayable_chunk->policy_ids.insert( - std::make_pair(policy_flags, std::set<ResourceId>{id})); + std::make_pair(item.policies, std::set<ResourceId>{id})); } } } @@ -546,7 +536,8 @@ class PackageFlattener { ChunkWriter policy_writer(buffer); auto* policy_type = policy_writer.StartChunk<ResTable_overlayable_policy_header>( RES_TABLE_OVERLAYABLE_POLICY_TYPE); - policy_type->policy_flags = util::HostToDevice32(static_cast<uint32_t>(policy_ids.first)); + policy_type->policy_flags = + static_cast<PolicyFlags>(util::HostToDevice32(static_cast<uint32_t>(policy_ids.first))); policy_type->entry_count = util::HostToDevice32(static_cast<uint32_t>( policy_ids.second.size())); // Write the ids after the policy header @@ -652,11 +643,12 @@ class PackageFlattener { for (ResourceEntry* entry : sorted_entries) { uint32_t local_key_index; + ResourceName resource_name({}, type->type, entry->name); if (!collapse_key_stringpool_ || - whitelisted_resources_.find(entry->name) != whitelisted_resources_.end()) { + name_collapse_exemptions_.find(resource_name) != name_collapse_exemptions_.end()) { local_key_index = (uint32_t)key_pool_.MakeRef(entry->name).index(); } else { - // resource isn't whitelisted, add it as obfuscated value + // resource isn't exempt from collapse, add it as obfuscated value local_key_index = (uint32_t)key_pool_.MakeRef(obfuscated_resource_name).index(); } // Group values by configuration. @@ -712,7 +704,7 @@ class PackageFlattener { StringPool type_pool_; StringPool key_pool_; bool collapse_key_stringpool_; - const std::set<std::string>& whitelisted_resources_; + const std::set<ResourceName>& name_collapse_exemptions_; }; } // namespace @@ -760,7 +752,7 @@ bool TableFlattener::Consume(IAaptContext* context, ResourceTable* table) { PackageFlattener flattener(context, package.get(), &table->included_packages_, options_.use_sparse_entries, options_.collapse_key_stringpool, - options_.whitelisted_resources); + options_.name_collapse_exemptions); if (!flattener.FlattenPackage(&package_buffer)) { return false; } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 73c17295556b..4360db190146 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -19,6 +19,7 @@ #include "android-base/macros.h" +#include "Resource.h" #include "ResourceTable.h" #include "process/IResourceTableConsumer.h" #include "util/BigBuffer.h" @@ -41,8 +42,8 @@ struct TableFlattenerOptions { // have name indices that point to this single value bool collapse_key_stringpool = false; - // Set of whitelisted resource names to avoid altering in key stringpool - std::set<std::string> whitelisted_resources; + // Set of resources to avoid collapsing to a single entry in key stringpool. + std::set<ResourceName> name_collapse_exemptions; // Map from original resource paths to shortened resource paths. std::map<std::string, std::string> shortened_path_map; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index a9409235e07a..59627ce579af 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -32,6 +32,8 @@ using ::testing::Gt; using ::testing::IsNull; using ::testing::NotNull; +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { class TableFlattenerTest : public ::testing::Test { @@ -431,6 +433,47 @@ TEST_F(TableFlattenerTest, FlattenSharedLibrary) { EXPECT_EQ("lib", iter->second); } +TEST_F(TableFlattenerTest, FlattenSharedLibraryWithStyle) { + std::unique_ptr<IAaptContext> context = + test::ContextBuilder().SetCompilationPackage("lib").SetPackageId(0x00).Build(); + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("lib", 0x00) + .AddValue("lib:style/Theme", + ResourceId(0x00030001), + test::StyleBuilder() + .AddItem("lib:attr/bar", ResourceId(0x00010002), + ResourceUtils::TryParseInt("2")) + .AddItem("lib:attr/foo", ResourceId(0x00010001), + ResourceUtils::TryParseInt("1")) + .AddItem("android:attr/bar", ResourceId(0x01010002), + ResourceUtils::TryParseInt("4")) + .AddItem("android:attr/foo", ResourceId(0x01010001), + ResourceUtils::TryParseInt("3")) + .Build()) + .Build(); + ResourceTable result; + ASSERT_TRUE(Flatten(context.get(), {}, table.get(), &result)); + + Maybe<ResourceTable::SearchResult> search_result = + result.FindResource(test::ParseNameOrDie("lib:style/Theme")); + ASSERT_TRUE(search_result); + EXPECT_EQ(0x00u, search_result.value().package->id.value()); + EXPECT_EQ(0x03u, search_result.value().type->id.value()); + EXPECT_EQ(0x01u, search_result.value().entry->id.value()); + ASSERT_EQ(1u, search_result.value().entry->values.size()); + Value* value = search_result.value().entry->values[0]->value.get(); + Style* style = ValueCast<Style>(value); + ASSERT_TRUE(style); + ASSERT_EQ(4u, style->entries.size()); + // Ensure the attributes from the shared library come after the items from + // android. + EXPECT_EQ(0x01010001, style->entries[0].key.id.value()); + EXPECT_EQ(0x01010002, style->entries[1].key.id.value()); + EXPECT_EQ(0x00010001, style->entries[2].key.id.value()); + EXPECT_EQ(0x00010002, style->entries[3].key.id.value()); +} + TEST_F(TableFlattenerTest, FlattenTableReferencingSharedLibraries) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().SetCompilationPackage("app").SetPackageId(0x7f).Build(); @@ -518,7 +561,7 @@ TEST_F(TableFlattenerTest, LongSharedLibraryPackageNameIsIllegal) { ASSERT_FALSE(Flatten(context.get(), {}, table.get(), &result)); } -TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) { +TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoNameCollapseExemptionsSucceeds) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() .SetPackageId("com.app.test", 0x7f) @@ -572,7 +615,7 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) { ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u)); } -TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) { +TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithNameCollapseExemptionsSucceeds) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() .SetPackageId("com.app.test", 0x7f) @@ -591,21 +634,22 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) { TableFlattenerOptions options; options.collapse_key_stringpool = true; - options.whitelisted_resources.insert("test"); - options.whitelisted_resources.insert("three"); + options.name_collapse_exemptions.insert(ResourceName({}, ResourceType::kId, "one")); + options.name_collapse_exemptions.insert(ResourceName({}, ResourceType::kString, "test")); ResTable res_table; ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table)); - EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated", + EXPECT_TRUE(Exists(&res_table, "com.app.test:id/one", ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated", ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); - EXPECT_TRUE(Exists(&res_table, "com.app.test:id/three", ResourceId(0x7f020002), {}, - Res_value::TYPE_REFERENCE, 0x7f020000u, 0u)); + EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated", + ResourceId(0x7f020002), {}, Res_value::TYPE_REFERENCE, 0x7f020000u, 0u)); + // Note that this resource is also named "one", but it's a different type, so gets obfuscated. EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated", ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u, ResTable_config::CONFIG_VERSION)); @@ -629,9 +673,9 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) { TEST_F(TableFlattenerTest, FlattenOverlayable) { OverlayableItem overlayable_item(std::make_shared<Overlayable>("TestName", "overlay://theme")); - overlayable_item.policies |= OverlayableItem::Policy::kProduct; - overlayable_item.policies |= OverlayableItem::Policy::kSystem; - overlayable_item.policies |= OverlayableItem::Policy::kVendor; + overlayable_item.policies |= PolicyFlags::PRODUCT_PARTITION; + overlayable_item.policies |= PolicyFlags::SYSTEM_PARTITION; + overlayable_item.policies |= PolicyFlags::VENDOR_PARTITION; std::string name = "com.app.test:integer/overlayable"; std::unique_ptr<ResourceTable> table = @@ -649,27 +693,27 @@ TEST_F(TableFlattenerTest, FlattenOverlayable) { ASSERT_THAT(search_result.value().entry, NotNull()); ASSERT_TRUE(search_result.value().entry->overlayable_item); OverlayableItem& result_overlayable_item = search_result.value().entry->overlayable_item.value(); - EXPECT_EQ(result_overlayable_item.policies, OverlayableItem::Policy::kSystem - | OverlayableItem::Policy::kVendor - | OverlayableItem::Policy::kProduct); + EXPECT_EQ(result_overlayable_item.policies, PolicyFlags::SYSTEM_PARTITION + | PolicyFlags::VENDOR_PARTITION + | PolicyFlags::PRODUCT_PARTITION); } TEST_F(TableFlattenerTest, FlattenMultipleOverlayablePolicies) { auto overlayable = std::make_shared<Overlayable>("TestName", "overlay://theme"); std::string name_zero = "com.app.test:integer/overlayable_zero_item"; OverlayableItem overlayable_item_zero(overlayable); - overlayable_item_zero.policies |= OverlayableItem::Policy::kProduct; - overlayable_item_zero.policies |= OverlayableItem::Policy::kSystem; + overlayable_item_zero.policies |= PolicyFlags::PRODUCT_PARTITION; + overlayable_item_zero.policies |= PolicyFlags::SYSTEM_PARTITION; std::string name_one = "com.app.test:integer/overlayable_one_item"; OverlayableItem overlayable_item_one(overlayable); - overlayable_item_one.policies |= OverlayableItem::Policy::kPublic; + overlayable_item_one.policies |= PolicyFlags::PUBLIC; std::string name_two = "com.app.test:integer/overlayable_two_item"; OverlayableItem overlayable_item_two(overlayable); - overlayable_item_two.policies |= OverlayableItem::Policy::kProduct; - overlayable_item_two.policies |= OverlayableItem::Policy::kSystem; - overlayable_item_two.policies |= OverlayableItem::Policy::kVendor; + overlayable_item_two.policies |= PolicyFlags::PRODUCT_PARTITION; + overlayable_item_two.policies |= PolicyFlags::SYSTEM_PARTITION; + overlayable_item_two.policies |= PolicyFlags::VENDOR_PARTITION; std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() @@ -690,47 +734,48 @@ TEST_F(TableFlattenerTest, FlattenMultipleOverlayablePolicies) { ASSERT_THAT(search_result.value().entry, NotNull()); ASSERT_TRUE(search_result.value().entry->overlayable_item); OverlayableItem& overlayable_item = search_result.value().entry->overlayable_item.value(); - EXPECT_EQ(overlayable_item.policies, OverlayableItem::Policy::kSystem - | OverlayableItem::Policy::kProduct); + EXPECT_EQ(overlayable_item.policies, PolicyFlags::SYSTEM_PARTITION + | PolicyFlags::PRODUCT_PARTITION); search_result = output_table.FindResource(test::ParseNameOrDie(name_one)); ASSERT_TRUE(search_result); ASSERT_THAT(search_result.value().entry, NotNull()); ASSERT_TRUE(search_result.value().entry->overlayable_item); overlayable_item = search_result.value().entry->overlayable_item.value(); - EXPECT_EQ(overlayable_item.policies, OverlayableItem::Policy::kPublic); + EXPECT_EQ(overlayable_item.policies, PolicyFlags::PUBLIC); search_result = output_table.FindResource(test::ParseNameOrDie(name_two)); ASSERT_TRUE(search_result); ASSERT_THAT(search_result.value().entry, NotNull()); ASSERT_TRUE(search_result.value().entry->overlayable_item); overlayable_item = search_result.value().entry->overlayable_item.value(); - EXPECT_EQ(overlayable_item.policies, OverlayableItem::Policy::kSystem - | OverlayableItem::Policy::kProduct - | OverlayableItem::Policy::kVendor); + EXPECT_EQ(overlayable_item.policies, PolicyFlags::SYSTEM_PARTITION + | PolicyFlags::PRODUCT_PARTITION + | PolicyFlags::VENDOR_PARTITION); } TEST_F(TableFlattenerTest, FlattenMultipleOverlayable) { auto group = std::make_shared<Overlayable>("TestName", "overlay://theme"); std::string name_zero = "com.app.test:integer/overlayable_zero"; OverlayableItem overlayable_item_zero(group); - overlayable_item_zero.policies |= OverlayableItem::Policy::kProduct; - overlayable_item_zero.policies |= OverlayableItem::Policy::kSystem; + overlayable_item_zero.policies |= PolicyFlags::PRODUCT_PARTITION; + overlayable_item_zero.policies |= PolicyFlags::SYSTEM_PARTITION; auto group_one = std::make_shared<Overlayable>("OtherName", "overlay://customization"); std::string name_one = "com.app.test:integer/overlayable_one"; OverlayableItem overlayable_item_one(group_one); - overlayable_item_one.policies |= OverlayableItem::Policy::kPublic; + overlayable_item_one.policies |= PolicyFlags::PUBLIC; std::string name_two = "com.app.test:integer/overlayable_two"; OverlayableItem overlayable_item_two(group); - overlayable_item_two.policies |= OverlayableItem::Policy::kOdm; - overlayable_item_two.policies |= OverlayableItem::Policy::kOem; - overlayable_item_two.policies |= OverlayableItem::Policy::kVendor; + overlayable_item_two.policies |= PolicyFlags::ODM_PARTITION; + overlayable_item_two.policies |= PolicyFlags::OEM_PARTITION; + overlayable_item_two.policies |= PolicyFlags::VENDOR_PARTITION; std::string name_three = "com.app.test:integer/overlayable_three"; OverlayableItem overlayable_item_three(group_one); - overlayable_item_three.policies |= OverlayableItem::Policy::kSignature; + overlayable_item_three.policies |= PolicyFlags::SIGNATURE; + overlayable_item_three.policies |= PolicyFlags::ACTOR_SIGNATURE; std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() @@ -754,8 +799,8 @@ TEST_F(TableFlattenerTest, FlattenMultipleOverlayable) { OverlayableItem& result_overlayable = search_result.value().entry->overlayable_item.value(); EXPECT_EQ(result_overlayable.overlayable->name, "TestName"); EXPECT_EQ(result_overlayable.overlayable->actor, "overlay://theme"); - EXPECT_EQ(result_overlayable.policies, OverlayableItem::Policy::kSystem - | OverlayableItem::Policy::kProduct); + EXPECT_EQ(result_overlayable.policies, PolicyFlags::SYSTEM_PARTITION + | PolicyFlags::PRODUCT_PARTITION); search_result = output_table.FindResource(test::ParseNameOrDie(name_one)); ASSERT_TRUE(search_result); @@ -764,7 +809,7 @@ TEST_F(TableFlattenerTest, FlattenMultipleOverlayable) { result_overlayable = search_result.value().entry->overlayable_item.value(); EXPECT_EQ(result_overlayable.overlayable->name, "OtherName"); EXPECT_EQ(result_overlayable.overlayable->actor, "overlay://customization"); - EXPECT_EQ(result_overlayable.policies, OverlayableItem::Policy::kPublic); + EXPECT_EQ(result_overlayable.policies, PolicyFlags::PUBLIC); search_result = output_table.FindResource(test::ParseNameOrDie(name_two)); ASSERT_TRUE(search_result); @@ -773,9 +818,9 @@ TEST_F(TableFlattenerTest, FlattenMultipleOverlayable) { result_overlayable = search_result.value().entry->overlayable_item.value(); EXPECT_EQ(result_overlayable.overlayable->name, "TestName"); EXPECT_EQ(result_overlayable.overlayable->actor, "overlay://theme"); - EXPECT_EQ(result_overlayable.policies, OverlayableItem::Policy::kOdm - | OverlayableItem::Policy::kOem - | OverlayableItem::Policy::kVendor); + EXPECT_EQ(result_overlayable.policies, PolicyFlags::ODM_PARTITION + | PolicyFlags::OEM_PARTITION + | PolicyFlags::VENDOR_PARTITION); search_result = output_table.FindResource(test::ParseNameOrDie(name_three)); ASSERT_TRUE(search_result); @@ -784,7 +829,8 @@ TEST_F(TableFlattenerTest, FlattenMultipleOverlayable) { result_overlayable = search_result.value().entry->overlayable_item.value(); EXPECT_EQ(result_overlayable.overlayable->name, "OtherName"); EXPECT_EQ(result_overlayable.overlayable->actor, "overlay://customization"); - EXPECT_EQ(result_overlayable.policies, OverlayableItem::Policy::kSignature); + EXPECT_EQ(result_overlayable.policies, PolicyFlags::SIGNATURE + | PolicyFlags::ACTOR_SIGNATURE); } TEST_F(TableFlattenerTest, FlattenOverlayableNoPolicyFails) { diff --git a/tools/aapt2/format/binary/XmlFlattener_test.cpp b/tools/aapt2/format/binary/XmlFlattener_test.cpp index 1dac493359e4..c24488b16153 100644 --- a/tools/aapt2/format/binary/XmlFlattener_test.cpp +++ b/tools/aapt2/format/binary/XmlFlattener_test.cpp @@ -226,10 +226,10 @@ TEST_F(XmlFlattenerTest, FlattenNonStandardPackageId) { ASSERT_TRUE(linker.Consume(context_.get(), doc.get())); // The tree needs a custom DynamicRefTable since it is not using a standard app ID (0x7f). - android::DynamicRefTable dynamic_ref_table; - dynamic_ref_table.addMapping(0x80, 0x80); + auto dynamic_ref_table = std::make_shared<android::DynamicRefTable>(); + dynamic_ref_table->addMapping(0x80, 0x80); - android::ResXMLTree tree(&dynamic_ref_table); + auto tree = android::ResXMLTree(std::move(dynamic_ref_table)); ASSERT_TRUE(Flatten(doc.get(), &tree)); while (tree.next() != android::ResXMLTree::START_TAG) { diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index bb21c1c539fb..2fd01d7f3dee 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -30,6 +30,8 @@ using ::android::ConfigDescription; using ::android::LocaleValue; using ::android::ResStringPool; +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { namespace { @@ -379,25 +381,28 @@ bool DeserializeOverlayableItemFromPb(const pb::OverlayableItem& pb_overlayable, for (const int policy : pb_overlayable.policy()) { switch (policy) { case pb::OverlayableItem::PUBLIC: - out_overlayable->policies |= OverlayableItem::Policy::kPublic; + out_overlayable->policies |= PolicyFlags::PUBLIC; break; case pb::OverlayableItem::SYSTEM: - out_overlayable->policies |= OverlayableItem::Policy::kSystem; + out_overlayable->policies |= PolicyFlags::SYSTEM_PARTITION; break; case pb::OverlayableItem::VENDOR: - out_overlayable->policies |= OverlayableItem::Policy::kVendor; + out_overlayable->policies |= PolicyFlags::VENDOR_PARTITION; break; case pb::OverlayableItem::PRODUCT: - out_overlayable->policies |= OverlayableItem::Policy::kProduct; + out_overlayable->policies |= PolicyFlags::PRODUCT_PARTITION; break; case pb::OverlayableItem::SIGNATURE: - out_overlayable->policies |= OverlayableItem::Policy::kSignature; + out_overlayable->policies |= PolicyFlags::SIGNATURE; break; case pb::OverlayableItem::ODM: - out_overlayable->policies |= OverlayableItem::Policy::kOdm; + out_overlayable->policies |= PolicyFlags::ODM_PARTITION; break; case pb::OverlayableItem::OEM: - out_overlayable->policies |= OverlayableItem::Policy::kOem; + out_overlayable->policies |= PolicyFlags::OEM_PARTITION; + break; + case pb::OverlayableItem::ACTOR: + out_overlayable->policies |= PolicyFlags::ACTOR_SIGNATURE; break; default: *out_error = "unknown overlayable policy"; @@ -634,6 +639,7 @@ static bool DeserializeReferenceFromPb(const pb::Reference& pb_ref, Reference* o std::string* out_error) { out_ref->reference_type = DeserializeReferenceTypeFromPb(pb_ref.type()); out_ref->private_reference = pb_ref.private_(); + out_ref->is_dynamic = pb_ref.is_dynamic().value(); if (pb_ref.id() != 0) { out_ref->id = ResourceId(pb_ref.id()); @@ -708,6 +714,8 @@ std::unique_ptr<Value> DeserializeValueFromPb(const pb::Value& pb_value, return {}; } symbol.value = pb_symbol.value(); + symbol.type = pb_symbol.type() != 0U ? pb_symbol.type() + : android::Res_value::TYPE_INT_DEC; attr->symbols.push_back(std::move(symbol)); } value = std::move(attr); diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index a54822b20302..ba6df22af9d3 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -21,6 +21,8 @@ using android::ConfigDescription; +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { void SerializeStringPoolToPb(const StringPool& pool, pb::StringPool* out_pb_pool, IDiagnostics* diag) { @@ -290,43 +292,51 @@ static void SerializeOverlayableItemToPb(const OverlayableItem& overlayable_item pb::Overlayable* pb_overlayable = pb_table->add_overlayable(); pb_overlayable->set_name(overlayable_item.overlayable->name); pb_overlayable->set_actor(overlayable_item.overlayable->actor); - SerializeSourceToPb(overlayable_item.overlayable->source, source_pool, - pb_overlayable->mutable_source()); + if (source_pool != nullptr) { + SerializeSourceToPb(overlayable_item.overlayable->source, source_pool, + pb_overlayable->mutable_source()); + } } pb::OverlayableItem* pb_overlayable_item = pb_entry->mutable_overlayable_item(); pb_overlayable_item->set_overlayable_idx(i); - if (overlayable_item.policies & OverlayableItem::Policy::kPublic) { + if (overlayable_item.policies & PolicyFlags::PUBLIC) { pb_overlayable_item->add_policy(pb::OverlayableItem::PUBLIC); } - if (overlayable_item.policies & OverlayableItem::Policy::kProduct) { + if (overlayable_item.policies & PolicyFlags::PRODUCT_PARTITION) { pb_overlayable_item->add_policy(pb::OverlayableItem::PRODUCT); } - if (overlayable_item.policies & OverlayableItem::Policy::kSystem) { + if (overlayable_item.policies & PolicyFlags::SYSTEM_PARTITION) { pb_overlayable_item->add_policy(pb::OverlayableItem::SYSTEM); } - if (overlayable_item.policies & OverlayableItem::Policy::kVendor) { + if (overlayable_item.policies & PolicyFlags::VENDOR_PARTITION) { pb_overlayable_item->add_policy(pb::OverlayableItem::VENDOR); } - if (overlayable_item.policies & OverlayableItem::Policy::kSignature) { + if (overlayable_item.policies & PolicyFlags::SIGNATURE) { pb_overlayable_item->add_policy(pb::OverlayableItem::SIGNATURE); } - if (overlayable_item.policies & OverlayableItem::Policy::kOdm) { + if (overlayable_item.policies & PolicyFlags::ODM_PARTITION) { pb_overlayable_item->add_policy(pb::OverlayableItem::ODM); } - if (overlayable_item.policies & OverlayableItem::Policy::kOem) { + if (overlayable_item.policies & PolicyFlags::OEM_PARTITION) { pb_overlayable_item->add_policy(pb::OverlayableItem::OEM); } + if (overlayable_item.policies & PolicyFlags::ACTOR_SIGNATURE) { + pb_overlayable_item->add_policy(pb::OverlayableItem::ACTOR); + } - SerializeSourceToPb(overlayable_item.source, source_pool, - pb_overlayable_item->mutable_source()); + if (source_pool != nullptr) { + SerializeSourceToPb(overlayable_item.source, source_pool, + pb_overlayable_item->mutable_source()); + } pb_overlayable_item->set_comment(overlayable_item.comment); } void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table, - IDiagnostics* diag) { - StringPool source_pool; + IDiagnostics* diag, SerializeTableOptions options) { + auto source_pool = (options.exclude_sources) ? nullptr : util::make_unique<StringPool>(); + pb::ToolFingerprint* pb_fingerprint = out_table->add_tool_fingerprint(); pb_fingerprint->set_tool(util::GetToolName()); pb_fingerprint->set_version(util::GetToolFingerprint()); @@ -356,32 +366,40 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table // Write the Visibility struct. pb::Visibility* pb_visibility = pb_entry->mutable_visibility(); pb_visibility->set_level(SerializeVisibilityToPb(entry->visibility.level)); - SerializeSourceToPb(entry->visibility.source, &source_pool, - pb_visibility->mutable_source()); + if (source_pool != nullptr) { + SerializeSourceToPb(entry->visibility.source, source_pool.get(), + pb_visibility->mutable_source()); + } pb_visibility->set_comment(entry->visibility.comment); if (entry->allow_new) { pb::AllowNew* pb_allow_new = pb_entry->mutable_allow_new(); - SerializeSourceToPb(entry->allow_new.value().source, &source_pool, - pb_allow_new->mutable_source()); + if (source_pool != nullptr) { + SerializeSourceToPb(entry->allow_new.value().source, source_pool.get(), + pb_allow_new->mutable_source()); + } pb_allow_new->set_comment(entry->allow_new.value().comment); } if (entry->overlayable_item) { - SerializeOverlayableItemToPb(entry->overlayable_item.value(), overlayables, &source_pool, - pb_entry, out_table); + SerializeOverlayableItemToPb(entry->overlayable_item.value(), overlayables, + source_pool.get(), pb_entry, out_table); } for (const std::unique_ptr<ResourceConfigValue>& config_value : entry->values) { pb::ConfigValue* pb_config_value = pb_entry->add_config_value(); SerializeConfig(config_value->config, pb_config_value->mutable_config()); pb_config_value->mutable_config()->set_product(config_value->product); - SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(), &source_pool); + SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(), + source_pool.get()); } } } } - SerializeStringPoolToPb(source_pool, out_table->mutable_source_pool(), diag); + + if (source_pool != nullptr) { + SerializeStringPoolToPb(*source_pool, out_table->mutable_source_pool(), diag); + } } static pb::Reference_Type SerializeReferenceTypeToPb(Reference::Type type) { @@ -405,6 +423,9 @@ static void SerializeReferenceToPb(const Reference& ref, pb::Reference* pb_ref) pb_ref->set_private_(ref.private_reference); pb_ref->set_type(SerializeReferenceTypeToPb(ref.reference_type)); + if (ref.is_dynamic) { + pb_ref->mutable_is_dynamic()->set_value(ref.is_dynamic); + } } template <typename T> @@ -552,6 +573,7 @@ class ValueSerializer : public ConstValueVisitor { SerializeItemMetaDataToPb(symbol.symbol, pb_symbol, src_pool_); SerializeReferenceToPb(symbol.symbol, pb_symbol->mutable_name()); pb_symbol->set_value(symbol.value); + pb_symbol->set_type(symbol.type); } } diff --git a/tools/aapt2/format/proto/ProtoSerialize.h b/tools/aapt2/format/proto/ProtoSerialize.h index 33ffd182435b..7a3ea9903732 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.h +++ b/tools/aapt2/format/proto/ProtoSerialize.h @@ -35,6 +35,11 @@ struct SerializeXmlOptions { bool remove_empty_text_nodes = false; }; +struct SerializeTableOptions { + /** Prevent serializing the source pool and source protos. */ + bool exclude_sources = false; +}; + // Serializes a Value to its protobuf representation. An optional StringPool will hold the // source path string. void SerializeValueToPb(const Value& value, pb::Value* out_value, StringPool* src_pool = nullptr); @@ -59,7 +64,8 @@ void SerializeStringPoolToPb(const StringPool& pool, pb::StringPool* out_pb_pool void SerializeConfig(const android::ConfigDescription& config, pb::Configuration* out_pb_config); // Serializes a ResourceTable into its protobuf representation. -void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table, IDiagnostics* diag); +void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table, + IDiagnostics* diag, SerializeTableOptions options = {}); // Serializes a ResourceFile into its protobuf representation. void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledFile* out_file); diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp index f252f33f44fb..1a7de6dc1c48 100644 --- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp @@ -28,6 +28,8 @@ using ::testing::NotNull; using ::testing::SizeIs; using ::testing::StrEq; +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { class MockFileCollection : public io::IFileCollection { @@ -80,7 +82,7 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { test::BuildPrimitive(android::Res_value::TYPE_INT_DEC, 123u), context->GetDiagnostics())); ASSERT_TRUE(table->AddResource( test::ParseNameOrDie("com.app.a:integer/one"), test::ParseConfigOrDie("land"), "tablet", - test::BuildPrimitive(android::Res_value::TYPE_INT_DEC, 321u), context->GetDiagnostics())); + test::BuildPrimitive(android::Res_value::TYPE_INT_HEX, 321u), context->GetDiagnostics())); // Make a reference with both resource name and resource ID. // The reference should point to a resource outside of this table to test that both name and id @@ -133,11 +135,13 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { &new_table, "com.app.a:integer/one", test::ParseConfigOrDie("land"), ""); ASSERT_THAT(prim, NotNull()); EXPECT_THAT(prim->value.data, Eq(123u)); + EXPECT_THAT(prim->value.dataType, Eq(0x10)); prim = test::GetValueForConfigAndProduct<BinaryPrimitive>( &new_table, "com.app.a:integer/one", test::ParseConfigOrDie("land"), "tablet"); ASSERT_THAT(prim, NotNull()); EXPECT_THAT(prim->value.data, Eq(321u)); + EXPECT_THAT(prim->value.dataType, Eq(0x11)); Reference* actual_ref = test::GetValue<Reference>(&new_table, "com.app.a:layout/abc"); ASSERT_THAT(actual_ref, NotNull()); @@ -169,7 +173,7 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { EXPECT_THAT(result_overlayable_item.overlayable->actor, Eq("overlay://theme")); EXPECT_THAT(result_overlayable_item.overlayable->source.path, Eq("res/values/overlayable.xml")); EXPECT_THAT(result_overlayable_item.overlayable->source.line, Eq(40)); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kNone)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::NONE)); EXPECT_THAT(result_overlayable_item.source.path, Eq("res/values/overlayable.xml")); EXPECT_THAT(result_overlayable_item.source.line, Eq(42)); } @@ -514,23 +518,28 @@ TEST(ProtoSerializeTest, SerializeDeserializeConfiguration) { TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { OverlayableItem overlayable_item_foo(std::make_shared<Overlayable>( "CustomizableResources", "overlay://customization")); - overlayable_item_foo.policies |= OverlayableItem::Policy::kSystem; - overlayable_item_foo.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_foo.policies |= PolicyFlags::SYSTEM_PARTITION; + overlayable_item_foo.policies |= PolicyFlags::PRODUCT_PARTITION; OverlayableItem overlayable_item_bar(std::make_shared<Overlayable>( "TaskBar", "overlay://theme")); - overlayable_item_bar.policies |= OverlayableItem::Policy::kPublic; - overlayable_item_bar.policies |= OverlayableItem::Policy::kVendor; + overlayable_item_bar.policies |= PolicyFlags::PUBLIC; + overlayable_item_bar.policies |= PolicyFlags::VENDOR_PARTITION; OverlayableItem overlayable_item_baz(std::make_shared<Overlayable>( "FontPack", "overlay://theme")); - overlayable_item_baz.policies |= OverlayableItem::Policy::kPublic; + overlayable_item_baz.policies |= PolicyFlags::PUBLIC; OverlayableItem overlayable_item_boz(std::make_shared<Overlayable>( "IconPack", "overlay://theme")); - overlayable_item_boz.policies |= OverlayableItem::Policy::kSignature; - overlayable_item_boz.policies |= OverlayableItem::Policy::kOdm; - overlayable_item_boz.policies |= OverlayableItem::Policy::kOem; + overlayable_item_boz.policies |= PolicyFlags::SIGNATURE; + overlayable_item_boz.policies |= PolicyFlags::ODM_PARTITION; + overlayable_item_boz.policies |= PolicyFlags::OEM_PARTITION; + + OverlayableItem overlayable_item_actor_config(std::make_shared<Overlayable>( + "ActorConfig", "overlay://theme")); + overlayable_item_actor_config.policies |= PolicyFlags::SIGNATURE; + overlayable_item_actor_config.policies |= PolicyFlags::ACTOR_SIGNATURE; OverlayableItem overlayable_item_biz(std::make_shared<Overlayable>( "Other", "overlay://customization")); @@ -544,6 +553,7 @@ TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { .SetOverlayable("com.app.a:bool/baz", overlayable_item_baz) .SetOverlayable("com.app.a:bool/boz", overlayable_item_boz) .SetOverlayable("com.app.a:bool/biz", overlayable_item_biz) + .SetOverlayable("com.app.a:bool/actor_config", overlayable_item_actor_config) .AddValue("com.app.a:bool/fiz", ResourceUtils::TryParseBool("true")) .Build(); @@ -563,8 +573,8 @@ TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { OverlayableItem& overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(overlayable_item.overlayable->name, Eq("CustomizableResources")); EXPECT_THAT(overlayable_item.overlayable->actor, Eq("overlay://customization")); - EXPECT_THAT(overlayable_item.policies, Eq(OverlayableItem::Policy::kSystem - | OverlayableItem::Policy::kProduct)); + EXPECT_THAT(overlayable_item.policies, Eq(PolicyFlags::SYSTEM_PARTITION + | PolicyFlags::PRODUCT_PARTITION)); search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/bar")); ASSERT_TRUE(search_result); @@ -572,8 +582,8 @@ TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(overlayable_item.overlayable->name, Eq("TaskBar")); EXPECT_THAT(overlayable_item.overlayable->actor, Eq("overlay://theme")); - EXPECT_THAT(overlayable_item.policies, Eq(OverlayableItem::Policy::kPublic - | OverlayableItem::Policy::kVendor)); + EXPECT_THAT(overlayable_item.policies, Eq(PolicyFlags::PUBLIC + | PolicyFlags::VENDOR_PARTITION)); search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/baz")); ASSERT_TRUE(search_result); @@ -581,7 +591,7 @@ TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(overlayable_item.overlayable->name, Eq("FontPack")); EXPECT_THAT(overlayable_item.overlayable->actor, Eq("overlay://theme")); - EXPECT_THAT(overlayable_item.policies, Eq(OverlayableItem::Policy::kPublic)); + EXPECT_THAT(overlayable_item.policies, Eq(PolicyFlags::PUBLIC)); search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/boz")); ASSERT_TRUE(search_result); @@ -589,16 +599,25 @@ TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(overlayable_item.overlayable->name, Eq("IconPack")); EXPECT_THAT(overlayable_item.overlayable->actor, Eq("overlay://theme")); - EXPECT_THAT(overlayable_item.policies, Eq(OverlayableItem::Policy::kSignature - | OverlayableItem::Policy::kOdm - | OverlayableItem::Policy::kOem)); + EXPECT_THAT(overlayable_item.policies, Eq(PolicyFlags::SIGNATURE + | PolicyFlags::ODM_PARTITION + | PolicyFlags::OEM_PARTITION)); + + search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/actor_config")); + ASSERT_TRUE(search_result); + ASSERT_TRUE(search_result.value().entry->overlayable_item); + overlayable_item = search_result.value().entry->overlayable_item.value(); + EXPECT_THAT(overlayable_item.overlayable->name, Eq("ActorConfig")); + EXPECT_THAT(overlayable_item.overlayable->actor, Eq("overlay://theme")); + EXPECT_THAT(overlayable_item.policies, Eq(PolicyFlags::SIGNATURE + | PolicyFlags::ACTOR_SIGNATURE)); search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/biz")); ASSERT_TRUE(search_result); ASSERT_TRUE(search_result.value().entry->overlayable_item); overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(overlayable_item.overlayable->name, Eq("Other")); - EXPECT_THAT(overlayable_item.policies, Eq(OverlayableItem::Policy::kNone)); + EXPECT_THAT(overlayable_item.policies, Eq(PolicyFlags::NONE)); EXPECT_THAT(overlayable_item.comment, Eq("comment")); search_result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/fiz")); @@ -606,4 +625,41 @@ TEST(ProtoSerializeTest, SerializeAndDeserializeOverlayable) { ASSERT_FALSE(search_result.value().entry->overlayable_item); } +TEST(ProtoSerializeTest, SerializeAndDeserializeDynamicReference) { + Reference ref(ResourceId(0x00010001)); + ref.is_dynamic = true; + + pb::Item pb_item; + SerializeItemToPb(ref, &pb_item); + + ASSERT_TRUE(pb_item.has_ref()); + EXPECT_EQ(pb_item.ref().id(), ref.id.value().id); + EXPECT_TRUE(pb_item.ref().is_dynamic().value()); + + std::unique_ptr<Item> item = DeserializeItemFromPb(pb_item, android::ResStringPool(), + android::ConfigDescription(), nullptr, + nullptr, nullptr); + Reference* actual_ref = ValueCast<Reference>(item.get()); + EXPECT_EQ(actual_ref->id.value().id, ref.id.value().id); + EXPECT_TRUE(actual_ref->is_dynamic); +} + +TEST(ProtoSerializeTest, SerializeAndDeserializeNonDynamicReference) { + Reference ref(ResourceId(0x00010001)); + + pb::Item pb_item; + SerializeItemToPb(ref, &pb_item); + + ASSERT_TRUE(pb_item.has_ref()); + EXPECT_EQ(pb_item.ref().id(), ref.id.value().id); + EXPECT_FALSE(pb_item.ref().has_is_dynamic()); + + std::unique_ptr<Item> item = DeserializeItemFromPb(pb_item, android::ResStringPool(), + android::ConfigDescription(), nullptr, + nullptr, nullptr); + Reference* actual_ref = ValueCast<Reference>(item.get()); + EXPECT_EQ(actual_ref->id.value().id, ref.id.value().id); + EXPECT_FALSE(actual_ref->is_dynamic); +} + } // namespace aapt diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk new file mode 100644 index 000000000000..6361f9b8ae7d --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk @@ -0,0 +1,2 @@ +LOCAL_PATH := $(call my-dir) +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk new file mode 100644 index 000000000000..6bc2064c6e63 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk @@ -0,0 +1,29 @@ +// +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_USE_AAPT2 := true +LOCAL_AAPT_NAMESPACES := true +LOCAL_PACKAGE_NAME := AaptTestMergeOnly_App +LOCAL_SDK_VERSION := current +LOCAL_EXPORT_PACKAGE_RESOURCES := true +LOCAL_MODULE_TAGS := tests +LOCAL_STATIC_ANDROID_LIBRARIES := \ + AaptTestMergeOnly_LeafLib \ + AaptTestMergeOnly_LocalLib +include $(BUILD_PACKAGE)
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/App/AndroidManifest.xml b/tools/aapt2/integration-tests/MergeOnlyTest/App/AndroidManifest.xml new file mode 100644 index 000000000000..bc3a7e5ebd21 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/App/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.app"> + + <application + android:label="@*com.local.lib:string/lib_string"/> + +</manifest>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk new file mode 100644 index 000000000000..7bf8cf84426c --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk @@ -0,0 +1,28 @@ +// +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_USE_AAPT2 := true +LOCAL_AAPT_NAMESPACES := true +LOCAL_MODULE := AaptTestMergeOnly_LeafLib +LOCAL_SDK_VERSION := current +LOCAL_MODULE_TAGS := tests +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res +LOCAL_MIN_SDK_VERSION := 21 +LOCAL_AAPT_FLAGS := --merge-only +include $(BUILD_STATIC_JAVA_LIBRARY)
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/AndroidManifest.xml b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/AndroidManifest.xml new file mode 100644 index 000000000000..9907bd98790d --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/AndroidManifest.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest package="com.leaf.lib" /> diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/res/layout/activity.xml b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/res/layout/activity.xml new file mode 100644 index 000000000000..07de87fa1d33 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/res/layout/activity.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:leaf="http://schemas.android.com/apk/res/com.leaf.lib"> + + <TextView android:text="@string/leaf_string" + leaf:leaf_attr="hello" + style="@style/LeafChildStyle"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/res/values/values.xml b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/res/values/values.xml new file mode 100644 index 000000000000..7f94c26de23c --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/res/values/values.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <attr format="string" name="leaf_attr"/> + <attr format="string" name="leaf_attr2"/> + + <string name="leaf_string">I am a leaf</string> + + <style name="LeafParentStyle"> + <item type="attr" name="leaf_attr"/> + <item type="attr" name="leaf_attr2"/> + </style> + + <style name="LeafChildStyle" parent="LeafParentStyle"> + <item type="attr" name="leaf_attr2">hello</item> + </style> + + <style name="LeafParentStyle.DottedChild"/> + + <declare-styleable name="leaf_ds"> + <attr name="leaf_attr">hello</attr> + </declare-styleable> + + <public type="attr" name="leaf_attr"/> + <public type="attr" name="leaf_attr2"/> + <public type="style" name="LeafParentStyle"/> + <public type="style" name="LeafChildStyle"/> + <public type="style" name="LeafParentStyle.DottedChild"/> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk new file mode 100644 index 000000000000..ba781c56a913 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk @@ -0,0 +1,28 @@ +// +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_USE_AAPT2 := true +LOCAL_AAPT_NAMESPACES := true +LOCAL_MODULE := AaptTestMergeOnly_LocalLib +LOCAL_SDK_VERSION := current +LOCAL_MODULE_TAGS := tests +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res +LOCAL_MIN_SDK_VERSION := 21 +LOCAL_AAPT_FLAGS := --merge-only +include $(BUILD_STATIC_JAVA_LIBRARY)
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/AndroidManifest.xml b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/AndroidManifest.xml new file mode 100644 index 000000000000..aa0ff5dcb4b6 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.local.lib"> + + <application> + + <activity + android:name="com.myapp.MyActivity" + android:theme="@com.leaf.lib:style/LeafParentStyle.DottedChild"/> + + </application> + +</manifest> diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/layout/activity.xml b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/layout/activity.xml new file mode 100644 index 000000000000..80d2fd6bcd09 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/layout/activity.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:leaf="http://schemas.android.com/apk/res/com.leaf.lib"> + + <TextView android:text="@*com.leaf.lib:string/leaf_string" + leaf:leaf_attr="hello" + style="@com.leaf.lib:style/LeafChildStyle"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/layout/includer.xml b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/layout/includer.xml new file mode 100644 index 000000000000..f06371874a45 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/layout/includer.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:leaf="http://schemas.android.com/apk/res/com.leaf.lib"> + + <include layout="@layout/activity"/> + + <include layout="@*com.leaf.lib:layout/activity"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/values/values.xml b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/values/values.xml new file mode 100644 index 000000000000..2f9704df0570 --- /dev/null +++ b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/res/values/values.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <string name="lib_string">@*com.leaf.lib:string/leaf_string</string> + + <style name="lib_style" parent="@com.leaf.lib:style/LeafChildStyle"> + <item name="com.leaf.lib:leaf_attr">hello</item> + </style> + + <style name="LeafParentStyle.DottedChild.LocalLibStyle" + parent="@com.leaf.lib:style/LeafParentStyle.DottedChild"/> + + <public type="style" name="LeafParentStyle.DottedChild.LocalLibStyle"/> + +</resources>
\ No newline at end of file diff --git a/tools/aapt2/io/Util.cpp b/tools/aapt2/io/Util.cpp index ce6d9352180d..bb925c9b3f8e 100644 --- a/tools/aapt2/io/Util.cpp +++ b/tools/aapt2/io/Util.cpp @@ -58,7 +58,7 @@ bool CopyFileToArchivePreserveCompression(IAaptContext* context, io::IFile* file return CopyFileToArchive(context, file, out_path, compression_flags, writer); } -bool CopyProtoToArchive(IAaptContext* context, ::google::protobuf::MessageLite* proto_msg, +bool CopyProtoToArchive(IAaptContext* context, ::google::protobuf::Message* proto_msg, const std::string& out_path, uint32_t compression_flags, IArchiveWriter* writer) { TRACE_CALL(); diff --git a/tools/aapt2/io/Util.h b/tools/aapt2/io/Util.h index 5f978a8e2c35..5cb8206db23c 100644 --- a/tools/aapt2/io/Util.h +++ b/tools/aapt2/io/Util.h @@ -19,7 +19,7 @@ #include <string> -#include "google/protobuf/message_lite.h" +#include "google/protobuf/message.h" #include "google/protobuf/io/coded_stream.h" #include "format/Archive.h" @@ -39,7 +39,7 @@ bool CopyFileToArchive(IAaptContext* context, IFile* file, const std::string& ou bool CopyFileToArchivePreserveCompression(IAaptContext* context, IFile* file, const std::string& out_path, IArchiveWriter* writer); -bool CopyProtoToArchive(IAaptContext* context, ::google::protobuf::MessageLite* proto_msg, +bool CopyProtoToArchive(IAaptContext* context, ::google::protobuf::Message* proto_msg, const std::string& out_path, uint32_t compression_flags, IArchiveWriter* writer); @@ -127,13 +127,13 @@ class ProtoInputStreamReader { public: explicit ProtoInputStreamReader(io::InputStream* in) : in_(in) { } - /** Deserializes a MessageLite proto from the current position in the input stream.*/ - template <typename T> bool ReadMessage(T *message_lite) { + /** Deserializes a Message proto from the current position in the input stream.*/ + template <typename T> bool ReadMessage(T *message) { ZeroCopyInputAdaptor adapter(in_); google::protobuf::io::CodedInputStream coded_stream(&adapter); coded_stream.SetTotalBytesLimit(std::numeric_limits<int32_t>::max(), coded_stream.BytesUntilTotalBytesLimit()); - return message_lite->ParseFromCodedStream(&coded_stream); + return message->ParseFromCodedStream(&coded_stream); } private: diff --git a/tools/aapt2/java/AnnotationProcessor.cpp b/tools/aapt2/java/AnnotationProcessor.cpp index bb667d6fa539..482d91aeb491 100644 --- a/tools/aapt2/java/AnnotationProcessor.cpp +++ b/tools/aapt2/java/AnnotationProcessor.cpp @@ -18,6 +18,7 @@ #include <algorithm> #include <array> +#include <regex> #include "text/Unicode.h" #include "text/Utf8Iterator.h" @@ -65,14 +66,26 @@ void AnnotationProcessor::AppendCommentLine(std::string comment) { // Treat deprecated specially, since we don't remove it from the source comment. if (comment.find(sDeprecated) != std::string::npos) { - annotation_bit_mask_ |= AnnotationRule::kDeprecated; + annotation_parameter_map_[AnnotationRule::kDeprecated] = ""; } for (const AnnotationRule& rule : sAnnotationRules) { std::string::size_type idx = comment.find(rule.doc_str.data()); if (idx != std::string::npos) { - annotation_bit_mask_ |= rule.bit_mask; - comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size()); + // Captures all parameters associated with the specified annotation rule + // by matching the first pair of parantheses after the rule. + std::regex re(rule.doc_str.to_string() + "\\s*\\((.+)\\)"); + std::smatch match_result; + const bool is_match = std::regex_search(comment, match_result, re); + // We currently only capture and preserve parameters for SystemApi. + if (is_match && rule.bit_mask == AnnotationRule::kSystemApi) { + annotation_parameter_map_[rule.bit_mask] = match_result[1].str(); + comment.erase(comment.begin() + match_result.position(), + comment.begin() + match_result.position() + match_result.length()); + } else { + annotation_parameter_map_[rule.bit_mask] = ""; + comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size()); + } } } @@ -119,7 +132,8 @@ void AnnotationProcessor::Print(Printer* printer, bool strip_api_annotations) co printer->Println(" */"); } - if (annotation_bit_mask_ & AnnotationRule::kDeprecated) { + if (annotation_parameter_map_.find(AnnotationRule::kDeprecated) != + annotation_parameter_map_.end()) { printer->Println("@Deprecated"); } @@ -127,8 +141,13 @@ void AnnotationProcessor::Print(Printer* printer, bool strip_api_annotations) co return; } for (const AnnotationRule& rule : sAnnotationRules) { - if (annotation_bit_mask_ & rule.bit_mask) { - printer->Println(rule.annotation); + const auto& it = annotation_parameter_map_.find(rule.bit_mask); + if (it != annotation_parameter_map_.end()) { + printer->Print(rule.annotation); + if (!it->second.empty()) { + printer->Print("(").Print(it->second).Print(")"); + } + printer->Print("\n"); } } } diff --git a/tools/aapt2/java/AnnotationProcessor.h b/tools/aapt2/java/AnnotationProcessor.h index 6d768be8e7dd..f217afb16f32 100644 --- a/tools/aapt2/java/AnnotationProcessor.h +++ b/tools/aapt2/java/AnnotationProcessor.h @@ -19,6 +19,7 @@ #include <sstream> #include <string> +#include <unordered_map> #include "androidfw/StringPiece.h" @@ -70,7 +71,7 @@ class AnnotationProcessor { std::stringstream comment_; std::stringstream mAnnotations; bool has_comments_ = false; - uint32_t annotation_bit_mask_ = 0; + std::unordered_map<uint32_t, std::string> annotation_parameter_map_; void AppendCommentLine(std::string line); }; diff --git a/tools/aapt2/java/AnnotationProcessor_test.cpp b/tools/aapt2/java/AnnotationProcessor_test.cpp index c4c8ff934348..6bc8902a6dcf 100644 --- a/tools/aapt2/java/AnnotationProcessor_test.cpp +++ b/tools/aapt2/java/AnnotationProcessor_test.cpp @@ -61,6 +61,21 @@ TEST(AnnotationProcessorTest, EmitsSystemApiAnnotationAndRemovesFromComment) { EXPECT_THAT(annotations, HasSubstr("This is a system API")); } +TEST(AnnotationProcessorTest, EmitsSystemApiAnnotationParamsAndRemovesFromComment) { + AnnotationProcessor processor; + processor.AppendComment("@SystemApi (p1=k1,p2=k2) This is a system API"); + + std::string annotations; + StringOutputStream out(&annotations); + Printer printer(&out); + processor.Print(&printer); + out.Flush(); + + EXPECT_THAT(annotations, HasSubstr("@android.annotation.SystemApi(p1=k1,p2=k2)")); + EXPECT_THAT(annotations, Not(HasSubstr("@SystemApi"))); + EXPECT_THAT(annotations, HasSubstr("This is a system API")); +} + TEST(AnnotationProcessorTest, EmitsTestApiAnnotationAndRemovesFromComment) { AnnotationProcessor processor; processor.AppendComment("@TestApi This is a test API"); diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp index 9788f64995fb..dffad3b99c06 100644 --- a/tools/aapt2/java/JavaClassGenerator.cpp +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -304,9 +304,11 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res auto documentation_remove_iter = std::remove_if(documentation_attrs.begin(), documentation_attrs.end(), [&](StyleableAttr entry) -> bool { - StringPiece attr_comment_line = entry.symbol.value().attribute->GetComment(); - return SkipSymbol(entry.symbol) || attr_comment_line.contains("@removed") - || attr_comment_line.contains("@hide"); + if (SkipSymbol(entry.symbol)) { + return true; + } + const StringPiece attr_comment_line = entry.symbol.value().attribute->GetComment(); + return attr_comment_line.contains("@removed") || attr_comment_line.contains("@hide"); }); documentation_attrs.erase(documentation_remove_iter, documentation_attrs.end()); @@ -428,7 +430,7 @@ void JavaClassGenerator::ProcessStyleable(const ResourceNameRef& name, const Res out_rewrite_method->AppendStatement( StringPrintf(" if ((styleable.%s[i] & 0xff000000) == 0) {", array_field_name.data())); out_rewrite_method->AppendStatement( - StringPrintf(" styleable.%s[i] = (styleable.%s[i] & 0x00ffffff) | (p << 24);", + StringPrintf(" styleable.%s[i] = (styleable.%s[i] & 0x00ffffff) | packageIdBits;", array_field_name.data(), array_field_name.data())); out_rewrite_method->AppendStatement(" }"); out_rewrite_method->AppendStatement("}"); @@ -487,9 +489,9 @@ void JavaClassGenerator::ProcessResource(const ResourceNameRef& name, const Reso if (out_rewrite_method != nullptr) { const StringPiece& type_str = to_string(name.type); - out_rewrite_method->AppendStatement(StringPrintf("%s.%s = (%s.%s & 0x00ffffff) | (p << 24);", - type_str.data(), field_name.data(), - type_str.data(), field_name.data())); + out_rewrite_method->AppendStatement( + StringPrintf("%s.%s = (%s.%s & 0x00ffffff) | packageIdBits;", type_str.data(), + field_name.data(), type_str.data(), field_name.data())); } } @@ -599,6 +601,7 @@ bool JavaClassGenerator::Generate(const StringPiece& package_name_to_generate, rewrite_method->AppendStatement( StringPrintf("%s.R.onResourcesLoaded(p);", package_to_callback.data())); } + rewrite_method->AppendStatement("final int packageIdBits = p << 24;"); } const bool is_public = (options_.types == JavaClassGeneratorOptions::SymbolTypes::kPublic); diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp index 4f51fc48c80e..1e1fe4740c6b 100644 --- a/tools/aapt2/java/JavaClassGenerator_test.cpp +++ b/tools/aapt2/java/JavaClassGenerator_test.cpp @@ -522,9 +522,15 @@ TEST(JavaClassGeneratorTest, GenerateOnResourcesLoadedCallbackForSharedLibrary) ASSERT_TRUE(generator.Generate("android", &out)); out.Flush(); - EXPECT_THAT(output, HasSubstr("void onResourcesLoaded")); - EXPECT_THAT(output, HasSubstr("com.foo.R.onResourcesLoaded")); - EXPECT_THAT(output, HasSubstr("com.boo.R.onResourcesLoaded")); + EXPECT_THAT(output, HasSubstr( + R"( public static void onResourcesLoaded(int p) { + com.foo.R.onResourcesLoaded(p); + com.boo.R.onResourcesLoaded(p); + final int packageIdBits = p << 24; + attr.foo = (attr.foo & 0x00ffffff) | packageIdBits; + id.foo = (id.foo & 0x00ffffff) | packageIdBits; + style.foo = (style.foo & 0x00ffffff) | packageIdBits; + })")); } TEST(JavaClassGeneratorTest, OnlyGenerateRText) { diff --git a/tools/aapt2/java/ProguardRules.cpp b/tools/aapt2/java/ProguardRules.cpp index ff7f5c12404c..d9a4caa34e0d 100644 --- a/tools/aapt2/java/ProguardRules.cpp +++ b/tools/aapt2/java/ProguardRules.cpp @@ -155,13 +155,19 @@ class MenuVisitor : public BaseVisitor { void Visit(xml::Element* node) override { if (node->namespace_uri.empty() && node->name == "item") { for (const auto& attr : node->attributes) { - if (attr.namespace_uri == xml::kSchemaAndroid) { - if ((attr.name == "actionViewClass" || attr.name == "actionProviderClass") && - util::IsJavaClassName(attr.value)) { - AddClass(node->line_number, attr.value, "android.content.Context"); - } else if (attr.name == "onClick") { - AddMethod(node->line_number, attr.value, "android.view.MenuItem"); - } + // AppCompat-v7 defines its own versions of Android attributes if + // they're defined after SDK 7 (the below are from 11 and 14, + // respectively), so don't bother checking the XML namespace. + // + // Given the names of the containing XML files and the attribute + // names, it's unlikely that keeping these classes would be wrong. + if ((attr.name == "actionViewClass" || attr.name == "actionProviderClass") && + util::IsJavaClassName(attr.value)) { + AddClass(node->line_number, attr.value, "android.content.Context"); + } + + if (attr.namespace_uri == xml::kSchemaAndroid && attr.name == "onClick") { + AddMethod(node->line_number, attr.value, "android.view.MenuItem"); } } } @@ -388,11 +394,15 @@ bool CollectProguardRules(IAaptContext* context_, xml::XmlResource* res, KeepSet return true; } -void WriteKeepSet(const KeepSet& keep_set, OutputStream* out, bool minimal_keep) { +void WriteKeepSet(const KeepSet& keep_set, OutputStream* out, bool minimal_keep, + bool no_location_reference) { + Printer printer(out); for (const auto& entry : keep_set.manifest_class_set_) { - for (const UsageLocation& location : entry.second) { - printer.Print("# Referenced at ").Println(location.source.to_string()); + if (!no_location_reference) { + for (const UsageLocation& location : entry.second) { + printer.Print("# Referenced at ").Println(location.source.to_string()); + } } printer.Print("-keep class ").Print(entry.first).Println(" { <init>(); }"); } @@ -409,7 +419,9 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out, bool minimal_keep) if (can_be_conditional) { for (const UsageLocation& location : locations) { - printer.Print("# Referenced at ").Println(location.source.to_string()); + if (!no_location_reference) { + printer.Print("# Referenced at ").Println(location.source.to_string()); + } printer.Print("-if class **.R$layout { int ") .Print(JavaClassGenerator::TransformToFieldName(location.name.entry)) .Println("; }"); @@ -419,8 +431,10 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out, bool minimal_keep) printer.Println("); }"); } } else { - for (const UsageLocation& location : entry.second) { - printer.Print("# Referenced at ").Println(location.source.to_string()); + if (!no_location_reference) { + for (const UsageLocation& location : entry.second) { + printer.Print("# Referenced at ").Println(location.source.to_string()); + } } printer.Print("-keep class ").Print(entry.first.name).Print(" { <init>("); @@ -431,8 +445,10 @@ void WriteKeepSet(const KeepSet& keep_set, OutputStream* out, bool minimal_keep) } for (const auto& entry : keep_set.method_set_) { - for (const UsageLocation& location : entry.second) { - printer.Print("# Referenced at ").Println(location.source.to_string()); + if (!no_location_reference) { + for (const UsageLocation& location : entry.second) { + printer.Print("# Referenced at ").Println(location.source.to_string()); + } } printer.Print("-keepclassmembers class * { *** ").Print(entry.first.name) .Print("(").Print(entry.first.signature).Println("); }"); diff --git a/tools/aapt2/java/ProguardRules.h b/tools/aapt2/java/ProguardRules.h index b15df59f56a6..a01b64d024d2 100644 --- a/tools/aapt2/java/ProguardRules.h +++ b/tools/aapt2/java/ProguardRules.h @@ -70,7 +70,8 @@ class KeepSet { } private: - friend void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out, bool minimal_keep); + friend void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out, bool minimal_keep, + bool no_location_reference); friend bool CollectLocations(const UsageLocation& location, const KeepSet& keep_set, std::set<UsageLocation>* locations); @@ -89,7 +90,8 @@ bool CollectProguardRules(IAaptContext* context, xml::XmlResource* res, KeepSet* bool CollectResourceReferences(IAaptContext* context, ResourceTable* table, KeepSet* keep_set); -void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out, bool minimal_keep); +void WriteKeepSet(const KeepSet& keep_set, io::OutputStream* out, bool minimal_keep, + bool no_location_reference); bool CollectLocations(const UsageLocation& location, const KeepSet& keep_set, std::set<UsageLocation>* locations); diff --git a/tools/aapt2/java/ProguardRules_test.cpp b/tools/aapt2/java/ProguardRules_test.cpp index 7c3cda089b40..c7ae0b6a75cc 100644 --- a/tools/aapt2/java/ProguardRules_test.cpp +++ b/tools/aapt2/java/ProguardRules_test.cpp @@ -30,7 +30,7 @@ namespace aapt { std::string GetKeepSetString(const proguard::KeepSet& set, bool minimal_rules) { std::string out; StringOutputStream sout(&out); - proguard::WriteKeepSet(set, &sout, minimal_rules); + proguard::WriteKeepSet(set, &sout, minimal_rules, false); sout.Flush(); return out; } @@ -381,6 +381,25 @@ TEST(ProguardRulesTest, MenuRulesAreEmitted) { EXPECT_THAT(actual, Not(HasSubstr("com.foo.Bat"))); } +TEST(ProguardRulesTest, MenuRulesAreEmittedForActionClasses) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> menu = test::BuildXmlDom(R"( + <menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item android:id="@+id/my_item" + app:actionViewClass="com.foo.Bar" + app:actionProviderClass="com.foo.Baz" /> + </menu>)"); + menu->file.name = test::ParseNameOrDie("menu/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules(context.get(), menu.get(), &set)); + + std::string actual = GetKeepSetString(set, /** minimal_rules */ false); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz")); +} + TEST(ProguardRulesTest, TransitionPathMotionRulesAreEmitted) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); std::unique_ptr<xml::XmlResource> transition = test::BuildXmlDom(R"( diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index 06303c273261..c813a446b8db 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -214,6 +214,33 @@ static bool VerifyUsesFeature(xml::Element* el, SourcePathDiagnostics* diag) { return true; } +// Ensure that 'ns_decls' contains a declaration for 'uri', using 'prefix' as +// the xmlns prefix if possible. +static void EnsureNamespaceIsDeclared(const std::string& prefix, const std::string& uri, + std::vector<xml::NamespaceDecl>* ns_decls) { + if (std::find_if(ns_decls->begin(), ns_decls->end(), [&](const xml::NamespaceDecl& ns_decl) { + return ns_decl.uri == uri; + }) != ns_decls->end()) { + return; + } + + std::set<std::string> used_prefixes; + for (const auto& ns_decl : *ns_decls) { + used_prefixes.insert(ns_decl.prefix); + } + + // Make multiple attempts in the unlikely event that 'prefix' is already taken. + std::string disambiguator; + for (int i = 0; i < used_prefixes.size() + 1; i++) { + std::string attempted_prefix = prefix + disambiguator; + if (used_prefixes.find(attempted_prefix) == used_prefixes.end()) { + ns_decls->push_back(xml::NamespaceDecl{attempted_prefix, uri}); + return; + } + disambiguator = std::to_string(i); + } +} + bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, IDiagnostics* diag) { // First verify some options. @@ -272,6 +299,8 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, manifest_action.Action(VerifyManifest); manifest_action.Action(FixCoreAppAttribute); manifest_action.Action([&](xml::Element* el) -> bool { + EnsureNamespaceIsDeclared("android", xml::kSchemaAndroid, &el->namespace_decls); + if (options_.version_name_default) { if (options_.replace_version) { el->RemoveAttribute(xml::kSchemaAndroid, "versionName"); @@ -330,6 +359,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, } return true; }); + manifest_action["uses-sdk"]["extension-sdk"]; // Instrumentation actions. manifest_action["instrumentation"].Action(RequiredNameIsJavaClassName); @@ -346,6 +376,12 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, }); manifest_action["instrumentation"]["meta-data"] = meta_data_action; + // TODO moltmann: Remove + manifest_action["feature"]; + manifest_action["feature"]["inherit-from"]; + + manifest_action["attribution"]; + manifest_action["attribution"]["inherit-from"]; manifest_action["original-package"]; manifest_action["overlay"].Action([&](xml::Element* el) -> bool { if (!options_.rename_overlay_target_package) { @@ -377,6 +413,10 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, manifest_action["package-verifier"]; manifest_action["meta-data"] = meta_data_action; manifest_action["uses-split"].Action(RequiredNameIsJavaPackage); + manifest_action["queries"]["package"].Action(RequiredNameIsJavaPackage); + manifest_action["queries"]["intent"] = intent_filter_action; + manifest_action["queries"]["provider"].Action(RequiredAndroidAttribute("authorities")); + // TODO: more complicated component name tag manifest_action["key-sets"]["key-set"]["public-key"]; manifest_action["key-sets"]["upgrade-key-set"]; @@ -413,6 +453,12 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, application_action["meta-data"] = meta_data_action; + application_action["processes"]; + application_action["processes"]["deny-permission"]; + application_action["processes"]["allow-permission"]; + application_action["processes"]["process"]["deny-permission"]; + application_action["processes"]["process"]["allow-permission"]; + application_action["activity"] = component_action; application_action["activity"]["layout"]; diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 3aba4e2ec49f..0791805e2506 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -753,8 +753,7 @@ TEST_F(ManifestFixerTest, SupportKeySets) { } TEST_F(ManifestFixerTest, InsertCompileSdkVersions) { - std::string input = R"( - <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android" />)"; + std::string input = R"(<manifest package="com.pkg" />)"; ManifestFixerOptions options; options.compile_sdk_version = {"28"}; options.compile_sdk_version_codename = {"P"}; @@ -762,6 +761,12 @@ TEST_F(ManifestFixerTest, InsertCompileSdkVersions) { std::unique_ptr<xml::XmlResource> manifest = VerifyWithOptions(input, options); ASSERT_THAT(manifest, NotNull()); + // There should be a declaration of kSchemaAndroid, even when the input + // didn't have one. + EXPECT_EQ(manifest->root->namespace_decls.size(), 1); + EXPECT_EQ(manifest->root->namespace_decls[0].prefix, "android"); + EXPECT_EQ(manifest->root->namespace_decls[0].uri, xml::kSchemaAndroid); + xml::Attribute* attr = manifest->root->FindAttribute(xml::kSchemaAndroid, "compileSdkVersion"); ASSERT_THAT(attr, NotNull()); EXPECT_THAT(attr->value, StrEq("28")); @@ -808,6 +813,27 @@ TEST_F(ManifestFixerTest, OverrideCompileSdkVersions) { EXPECT_THAT(attr->value, StrEq("P")); } +TEST_F(ManifestFixerTest, AndroidPrefixAlreadyUsed) { + std::string input = + R"(<manifest package="com.pkg" + xmlns:android="http://schemas.android.com/apk/prv/res/android" + android:private_attr="foo" />)"; + ManifestFixerOptions options; + options.compile_sdk_version = {"28"}; + options.compile_sdk_version_codename = {"P"}; + + std::unique_ptr<xml::XmlResource> manifest = VerifyWithOptions(input, options); + ASSERT_THAT(manifest, NotNull()); + + // Make sure that we don't redefine "android". + EXPECT_EQ(manifest->root->namespace_decls.size(), 2); + EXPECT_EQ(manifest->root->namespace_decls[0].prefix, "android"); + EXPECT_EQ(manifest->root->namespace_decls[0].uri, + "http://schemas.android.com/apk/prv/res/android"); + EXPECT_EQ(manifest->root->namespace_decls[1].prefix, "android0"); + EXPECT_EQ(manifest->root->namespace_decls[1].uri, xml::kSchemaAndroid); +} + TEST_F(ManifestFixerTest, UnexpectedElementsInManifest) { std::string input = R"( <manifest xmlns:android="http://schemas.android.com/apk/res/android" diff --git a/tools/aapt2/link/ReferenceLinker.cpp b/tools/aapt2/link/ReferenceLinker.cpp index 28f09aa48365..8e49fabe6a5c 100644 --- a/tools/aapt2/link/ReferenceLinker.cpp +++ b/tools/aapt2/link/ReferenceLinker.cpp @@ -17,6 +17,7 @@ #include "link/ReferenceLinker.h" #include "android-base/logging.h" +#include "android-base/stringprintf.h" #include "androidfw/ResourceTypes.h" #include "Diagnostics.h" @@ -33,6 +34,7 @@ using ::aapt::ResourceUtils::StringBuilder; using ::android::StringPiece; +using ::android::base::StringPrintf; namespace aapt { @@ -81,7 +83,7 @@ class ReferenceLinkerVisitor : public DescendingValueVisitor { // Find the attribute in the symbol table and check if it is visible from this callsite. const SymbolTable::Symbol* symbol = ReferenceLinker::ResolveAttributeCheckVisibility( - transformed_reference, callsite_, symbols_, &err_str); + transformed_reference, callsite_, context_, symbols_, &err_str); if (symbol) { // Assign our style key the correct ID. The ID may not exist. entry.key.id = symbol->id; @@ -203,12 +205,35 @@ bool IsSymbolVisible(const SymbolTable::Symbol& symbol, const Reference& ref, const SymbolTable::Symbol* ReferenceLinker::ResolveSymbol(const Reference& reference, const CallSite& callsite, + IAaptContext* context, SymbolTable* symbols) { if (reference.name) { const ResourceName& name = reference.name.value(); if (name.package.empty()) { // Use the callsite's package name if no package name was defined. - return symbols->FindByName(ResourceName(callsite.package, name.type, name.entry)); + const SymbolTable::Symbol* symbol = symbols->FindByName( + ResourceName(callsite.package, name.type, name.entry)); + if (symbol) { + return symbol; + } + + // If the callsite package is the same as the current compilation package, + // check the feature split dependencies as well. Feature split resources + // can be referenced without a namespace, just like the base package. + // TODO: modify the package name of included splits instead of having the + // symbol table look up the resource in in every package. b/136105066 + if (callsite.package == context->GetCompilationPackage()) { + const auto& split_name_dependencies = context->GetSplitNameDependencies(); + for (const std::string& split_name : split_name_dependencies) { + std::string split_package = + StringPrintf("%s.%s", callsite.package.c_str(), split_name.c_str()); + symbol = symbols->FindByName(ResourceName(split_package, name.type, name.entry)); + if (symbol) { + return symbol; + } + } + } + return nullptr; } return symbols->FindByName(name); } else if (reference.id) { @@ -220,9 +245,10 @@ const SymbolTable::Symbol* ReferenceLinker::ResolveSymbol(const Reference& refer const SymbolTable::Symbol* ReferenceLinker::ResolveSymbolCheckVisibility(const Reference& reference, const CallSite& callsite, + IAaptContext* context, SymbolTable* symbols, std::string* out_error) { - const SymbolTable::Symbol* symbol = ResolveSymbol(reference, callsite, symbols); + const SymbolTable::Symbol* symbol = ResolveSymbol(reference, callsite, context, symbols); if (!symbol) { if (out_error) *out_error = "not found"; return nullptr; @@ -236,10 +262,10 @@ const SymbolTable::Symbol* ReferenceLinker::ResolveSymbolCheckVisibility(const R } const SymbolTable::Symbol* ReferenceLinker::ResolveAttributeCheckVisibility( - const Reference& reference, const CallSite& callsite, SymbolTable* symbols, - std::string* out_error) { + const Reference& reference, const CallSite& callsite, IAaptContext* context, + SymbolTable* symbols, std::string* out_error) { const SymbolTable::Symbol* symbol = - ResolveSymbolCheckVisibility(reference, callsite, symbols, out_error); + ResolveSymbolCheckVisibility(reference, callsite, context, symbols, out_error); if (!symbol) { return nullptr; } @@ -253,10 +279,11 @@ const SymbolTable::Symbol* ReferenceLinker::ResolveAttributeCheckVisibility( Maybe<xml::AaptAttribute> ReferenceLinker::CompileXmlAttribute(const Reference& reference, const CallSite& callsite, + IAaptContext* context, SymbolTable* symbols, std::string* out_error) { const SymbolTable::Symbol* symbol = - ResolveAttributeCheckVisibility(reference, callsite, symbols, out_error); + ResolveAttributeCheckVisibility(reference, callsite, context, symbols, out_error); if (!symbol) { return {}; } @@ -335,7 +362,7 @@ bool ReferenceLinker::LinkReference(const CallSite& callsite, Reference* referen std::string err_str; const SymbolTable::Symbol* s = - ResolveSymbolCheckVisibility(transformed_reference, callsite, symbols, &err_str); + ResolveSymbolCheckVisibility(transformed_reference, callsite, context, symbols, &err_str); if (s) { // The ID may not exist. This is fine because of the possibility of building // against libraries without assigned IDs. diff --git a/tools/aapt2/link/ReferenceLinker.h b/tools/aapt2/link/ReferenceLinker.h index b0b49457e5dd..1256709edbf4 100644 --- a/tools/aapt2/link/ReferenceLinker.h +++ b/tools/aapt2/link/ReferenceLinker.h @@ -39,13 +39,16 @@ class ReferenceLinker : public IResourceTableConsumer { // package if the reference has no package name defined (implicit). // Returns nullptr if the symbol was not found. static const SymbolTable::Symbol* ResolveSymbol(const Reference& reference, - const CallSite& callsite, SymbolTable* symbols); + const CallSite& callsite, + IAaptContext* context, + SymbolTable* symbols); // Performs name mangling and looks up the resource in the symbol table. If the symbol is not // visible by the reference at the callsite, nullptr is returned. // `out_error` holds the error message. static const SymbolTable::Symbol* ResolveSymbolCheckVisibility(const Reference& reference, const CallSite& callsite, + IAaptContext* context, SymbolTable* symbols, std::string* out_error); @@ -53,6 +56,7 @@ class ReferenceLinker : public IResourceTableConsumer { // That is, the return value will have a non-null value for ISymbolTable::Symbol::attribute. static const SymbolTable::Symbol* ResolveAttributeCheckVisibility(const Reference& reference, const CallSite& callsite, + IAaptContext* context, SymbolTable* symbols, std::string* out_error); @@ -60,6 +64,7 @@ class ReferenceLinker : public IResourceTableConsumer { // If resolution fails, outError holds the error message. static Maybe<xml::AaptAttribute> CompileXmlAttribute(const Reference& reference, const CallSite& callsite, + IAaptContext* context, SymbolTable* symbols, std::string* out_error); diff --git a/tools/aapt2/link/ReferenceLinker_test.cpp b/tools/aapt2/link/ReferenceLinker_test.cpp index be38b967c986..a31ce9496d0c 100644 --- a/tools/aapt2/link/ReferenceLinker_test.cpp +++ b/tools/aapt2/link/ReferenceLinker_test.cpp @@ -266,8 +266,13 @@ TEST(ReferenceLinkerTest, AppsWithSamePackageButDifferentIdAreVisibleNonPublic) std::string error; const CallSite call_site{"com.app.test"}; + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .SetCompilationPackage("com.app.test") + .SetPackageId(0x7f) + .Build(); const SymbolTable::Symbol* symbol = ReferenceLinker::ResolveSymbolCheckVisibility( - *test::BuildReference("com.app.test:string/foo"), call_site, &table, &error); + *test::BuildReference("com.app.test:string/foo"), call_site, context.get(), &table, &error); ASSERT_THAT(symbol, NotNull()); EXPECT_TRUE(error.empty()); } @@ -281,17 +286,23 @@ TEST(ReferenceLinkerTest, AppsWithDifferentPackageCanNotUseEachOthersAttribute) .AddPublicSymbol("com.app.test:attr/public_foo", ResourceId(0x7f010001), test::AttributeBuilder().Build()) .Build()); + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .SetCompilationPackage("com.app.ext") + .SetPackageId(0x7f) + .Build(); std::string error; const CallSite call_site{"com.app.ext"}; EXPECT_FALSE(ReferenceLinker::CompileXmlAttribute( - *test::BuildReference("com.app.test:attr/foo"), call_site, &table, &error)); + *test::BuildReference("com.app.test:attr/foo"), call_site, context.get(), &table, &error)); EXPECT_FALSE(error.empty()); error = ""; ASSERT_TRUE(ReferenceLinker::CompileXmlAttribute( - *test::BuildReference("com.app.test:attr/public_foo"), call_site, &table, &error)); + *test::BuildReference("com.app.test:attr/public_foo"), call_site, context.get(), &table, + &error)); EXPECT_TRUE(error.empty()); } @@ -302,20 +313,62 @@ TEST(ReferenceLinkerTest, ReferenceWithNoPackageUsesCallSitePackage) { .AddSymbol("com.app.test:string/foo", ResourceId(0x7f010000)) .AddSymbol("com.app.lib:string/foo", ResourceId(0x7f010001)) .Build()); + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .SetCompilationPackage("com.app.test") + .SetPackageId(0x7f) + .Build(); const SymbolTable::Symbol* s = ReferenceLinker::ResolveSymbol(*test::BuildReference("string/foo"), - CallSite{"com.app.test"}, &table); + CallSite{"com.app.test"}, + context.get(), &table); ASSERT_THAT(s, NotNull()); EXPECT_THAT(s->id, Eq(make_value<ResourceId>(0x7f010000))); s = ReferenceLinker::ResolveSymbol(*test::BuildReference("string/foo"), CallSite{"com.app.lib"}, - &table); + context.get(), &table); ASSERT_THAT(s, NotNull()); EXPECT_THAT(s->id, Eq(make_value<ResourceId>(0x7f010001))); EXPECT_THAT(ReferenceLinker::ResolveSymbol(*test::BuildReference("string/foo"), - CallSite{"com.app.bad"}, &table), + CallSite{"com.app.bad"}, context.get(), &table), IsNull()); } +TEST(ReferenceLinkerTest, ReferenceSymbolFromOtherSplit) { + NameMangler mangler(NameManglerPolicy{"com.app.test"}); + SymbolTable table(&mangler); + table.AppendSource(test::StaticSymbolSourceBuilder() + .AddSymbol("com.app.test.feature:string/bar", ResourceId(0x80010000)) + .Build()); + std::set<std::string> split_name_dependencies; + split_name_dependencies.insert("feature"); + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .SetCompilationPackage("com.app.test") + .SetPackageId(0x81) + .SetSplitNameDependencies(split_name_dependencies) + .Build(); + + const SymbolTable::Symbol* s = ReferenceLinker::ResolveSymbol(*test::BuildReference("string/bar"), + CallSite{"com.app.test"}, + context.get(), &table); + ASSERT_THAT(s, NotNull()); + EXPECT_THAT(s->id, Eq(make_value<ResourceId>(0x80010000))); + + s = ReferenceLinker::ResolveSymbol(*test::BuildReference("string/foo"), CallSite{"com.app.lib"}, + context.get(), &table); + EXPECT_THAT(s, IsNull()); + + context = + test::ContextBuilder() + .SetCompilationPackage("com.app.test") + .SetPackageId(0x81) + .Build(); + s = ReferenceLinker::ResolveSymbol(*test::BuildReference("string/bar"),CallSite{"com.app.test"}, + context.get(), &table); + + EXPECT_THAT(s, IsNull()); +} + } // namespace aapt diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index 3f65e868505d..c25e4503a208 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -172,28 +172,32 @@ static bool MergeEntry(IAaptContext* context, const Source& src, // // Styleables and Styles don't simply overlay each other, their definitions merge and accumulate. // If both values are Styleables/Styles, we just merge them into the existing value. -static ResourceTable::CollisionResult ResolveMergeCollision(Value* existing, Value* incoming, - StringPool* pool) { +static ResourceTable::CollisionResult ResolveMergeCollision( + bool override_styles_instead_of_overlaying, Value* existing, Value* incoming, + StringPool* pool) { if (Styleable* existing_styleable = ValueCast<Styleable>(existing)) { if (Styleable* incoming_styleable = ValueCast<Styleable>(incoming)) { // Styleables get merged. existing_styleable->MergeWith(incoming_styleable); return ResourceTable::CollisionResult::kKeepOriginal; } - } else if (Style* existing_style = ValueCast<Style>(existing)) { - if (Style* incoming_style = ValueCast<Style>(incoming)) { - // Styles get merged. - existing_style->MergeWith(incoming_style, pool); - return ResourceTable::CollisionResult::kKeepOriginal; + } else if (!override_styles_instead_of_overlaying) { + if (Style* existing_style = ValueCast<Style>(existing)) { + if (Style* incoming_style = ValueCast<Style>(incoming)) { + // Styles get merged. + existing_style->MergeWith(incoming_style, pool); + return ResourceTable::CollisionResult::kKeepOriginal; + } } } // Delegate to the default handler. - return ResourceTable::ResolveValueCollision(existing, incoming, true /* overlay */); + return ResourceTable::ResolveValueCollision(existing, incoming); } static ResourceTable::CollisionResult MergeConfigValue(IAaptContext* context, const ResourceNameRef& res_name, bool overlay, + bool override_styles_instead_of_overlaying, ResourceConfigValue* dst_config_value, ResourceConfigValue* src_config_value, StringPool* pool) { @@ -204,13 +208,18 @@ static ResourceTable::CollisionResult MergeConfigValue(IAaptContext* context, CollisionResult collision_result; if (overlay) { - collision_result = ResolveMergeCollision(dst_value, src_value, pool); + collision_result = + ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool); } else { - collision_result = ResourceTable::ResolveValueCollision(dst_value, src_value, - false /* overlay */); + collision_result = ResourceTable::ResolveValueCollision(dst_value, src_value); } if (collision_result == CollisionResult::kConflict) { + if (overlay) { + return CollisionResult::kTakeNew; + } + + // Error! context->GetDiagnostics()->Error(DiagMessage(src_value->GetSource()) << "resource '" << res_name << "' has a conflicting value for " << "configuration (" << src_config_value->config << ")"); @@ -268,9 +277,9 @@ bool TableMerger::DoMerge(const Source& src, ResourceTablePackage* src_package, ResourceConfigValue* dst_config_value = dst_entry->FindValue( src_config_value->config, src_config_value->product); if (dst_config_value) { - CollisionResult collision_result = - MergeConfigValue(context_, res_name, overlay, dst_config_value, - src_config_value.get(), &master_table_->string_pool); + CollisionResult collision_result = MergeConfigValue( + context_, res_name, overlay, options_.override_styles_instead_of_overlaying, + dst_config_value, src_config_value.get(), &master_table_->string_pool); if (collision_result == CollisionResult::kConflict) { error = true; continue; diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h index 51305cfcdd25..a35a134a887d 100644 --- a/tools/aapt2/link/TableMerger.h +++ b/tools/aapt2/link/TableMerger.h @@ -37,6 +37,8 @@ struct TableMergerOptions { bool auto_add_overlay = false; // If true, resource overlays with conflicting visibility are not allowed. bool strict_visibility = false; + // If true, styles specified via "aapt2 link -R" completely replace any previously-seen resources. + bool override_styles_instead_of_overlaying = false; }; // TableMerger takes resource tables and merges all packages within the tables that have the same diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp index 78d42a160e21..69cf5ee7002b 100644 --- a/tools/aapt2/link/TableMerger_test.cpp +++ b/tools/aapt2/link/TableMerger_test.cpp @@ -29,6 +29,8 @@ using ::testing::Pointee; using ::testing::StrEq; using ::testing::UnorderedElementsAreArray; +using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; + namespace aapt { struct TableMergerTest : public ::testing::Test { @@ -352,62 +354,6 @@ TEST_F(TableMergerTest, MergeAddResourceFromOverlayWithAutoAddOverlay) { ASSERT_TRUE(merger.Merge({}, table_b.get(), false /*overlay*/)); } -TEST_F(TableMergerTest, OverrideAttributeSameFormatsWithOverlay) { - std::unique_ptr<ResourceTable> base = - test::ResourceTableBuilder() - .SetPackageId("", 0x7f) - .AddValue("attr/foo", test::AttributeBuilder() - .SetTypeMask(android::ResTable_map::TYPE_STRING) - .SetWeak(false) - .Build()) - .Build(); - - std::unique_ptr<ResourceTable> overlay = - test::ResourceTableBuilder() - .SetPackageId("", 0x7f) - .AddValue("attr/foo", test::AttributeBuilder() - .SetTypeMask(android::ResTable_map::TYPE_STRING) - .SetWeak(false) - .Build()) - .Build(); - - ResourceTable final_table; - TableMergerOptions options; - options.auto_add_overlay = false; - TableMerger merger(context_.get(), &final_table, options); - - ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); - ASSERT_TRUE(merger.Merge({}, overlay.get(), true /*overlay*/)); -} - -TEST_F(TableMergerTest, FailToOverrideConflictingAttributeFormatsWithOverlay) { - std::unique_ptr<ResourceTable> base = - test::ResourceTableBuilder() - .SetPackageId("", 0x7f) - .AddValue("attr/foo", test::AttributeBuilder() - .SetTypeMask(android::ResTable_map::TYPE_ANY) - .SetWeak(false) - .Build()) - .Build(); - - std::unique_ptr<ResourceTable> overlay = - test::ResourceTableBuilder() - .SetPackageId("", 0x7f) - .AddValue("attr/foo", test::AttributeBuilder() - .SetTypeMask(android::ResTable_map::TYPE_STRING) - .SetWeak(false) - .Build()) - .Build(); - - ResourceTable final_table; - TableMergerOptions options; - options.auto_add_overlay = false; - TableMerger merger(context_.get(), &final_table, options); - - ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); - ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); -} - TEST_F(TableMergerTest, FailToMergeNewResourceWithoutAutoAddOverlay) { std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder().SetPackageId("", 0x7f).Build(); @@ -492,12 +438,59 @@ TEST_F(TableMergerTest, OverlaidStyleablesAndStylesShouldBeMerged) { Eq(make_value(Reference(test::ParseNameOrDie("com.app.a:style/OverlayParent"))))); } +TEST_F(TableMergerTest, OverrideStyleInsteadOfOverlaying) { + std::unique_ptr<ResourceTable> table_a = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddValue( + "com.app.a:styleable/MyWidget", + test::StyleableBuilder().AddItem("com.app.a:attr/foo", ResourceId(0x1234)).Build()) + .AddValue("com.app.a:style/Theme", + test::StyleBuilder() + .AddItem("com.app.a:attr/foo", ResourceUtils::MakeBool(false)) + .Build()) + .Build(); + std::unique_ptr<ResourceTable> table_b = + test::ResourceTableBuilder() + .SetPackageId("com.app.a", 0x7f) + .AddValue( + "com.app.a:styleable/MyWidget", + test::StyleableBuilder().AddItem("com.app.a:attr/bar", ResourceId(0x5678)).Build()) + .AddValue( + "com.app.a:style/Theme", + test::StyleBuilder().AddItem("com.app.a:attr/bat", util::make_unique<Id>()).Build()) + .Build(); + + ResourceTable final_table; + TableMergerOptions options; + options.auto_add_overlay = true; + options.override_styles_instead_of_overlaying = true; + TableMerger merger(context_.get(), &final_table, options); + ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); + ASSERT_TRUE(merger.Merge({}, table_b.get(), true /*overlay*/)); + + // Styleables are always overlaid + std::unique_ptr<Styleable> expected_styleable = test::StyleableBuilder() + // The merged Styleable has its entries ordered by name. + .AddItem("com.app.a:attr/bar", ResourceId(0x5678)) + .AddItem("com.app.a:attr/foo", ResourceId(0x1234)) + .Build(); + const Styleable* actual_styleable = + test::GetValue<Styleable>(&final_table, "com.app.a:styleable/MyWidget"); + ASSERT_NE(actual_styleable, nullptr); + EXPECT_TRUE(actual_styleable->Equals(expected_styleable.get())); + // Style should be overridden + const Style* actual_style = test::GetValue<Style>(&final_table, "com.app.a:style/Theme"); + ASSERT_NE(actual_style, nullptr); + EXPECT_TRUE(actual_style->Equals(test::GetValue<Style>(table_b.get(), "com.app.a:style/Theme"))); +} + TEST_F(TableMergerTest, SetOverlayable) { auto overlayable = std::make_shared<Overlayable>("CustomizableResources", "overlay://customization"); OverlayableItem overlayable_item(overlayable); - overlayable_item.policies |= OverlayableItem::Policy::kProduct; - overlayable_item.policies |= OverlayableItem::Policy::kVendor; + overlayable_item.policies |= PolicyFlags::PRODUCT_PARTITION; + overlayable_item.policies |= PolicyFlags::VENDOR_PARTITION; std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder() @@ -525,8 +518,8 @@ TEST_F(TableMergerTest, SetOverlayable) { OverlayableItem& result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("CustomizableResources")); EXPECT_THAT(result_overlayable_item.overlayable->actor, Eq("overlay://customization")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kProduct - | OverlayableItem::Policy::kVendor)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::PRODUCT_PARTITION + | PolicyFlags::VENDOR_PARTITION)); } TEST_F(TableMergerTest, SetOverlayableLater) { @@ -539,8 +532,8 @@ TEST_F(TableMergerTest, SetOverlayableLater) { .Build(); OverlayableItem overlayable_item(overlayable); - overlayable_item.policies |= OverlayableItem::Policy::kPublic; - overlayable_item.policies |= OverlayableItem::Policy::kSystem; + overlayable_item.policies |= PolicyFlags::PUBLIC; + overlayable_item.policies |= PolicyFlags::SYSTEM_PARTITION; std::unique_ptr<ResourceTable> table_b = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -561,15 +554,15 @@ TEST_F(TableMergerTest, SetOverlayableLater) { OverlayableItem& result_overlayable_item = search_result.value().entry->overlayable_item.value(); EXPECT_THAT(result_overlayable_item.overlayable->name, Eq("CustomizableResources")); EXPECT_THAT(result_overlayable_item.overlayable->actor, Eq("overlay://customization")); - EXPECT_THAT(result_overlayable_item.policies, Eq(OverlayableItem::Policy::kPublic - | OverlayableItem::Policy::kSystem)); + EXPECT_THAT(result_overlayable_item.policies, Eq(PolicyFlags::PUBLIC + | PolicyFlags::SYSTEM_PARTITION)); } TEST_F(TableMergerTest, SameResourceDifferentNameFail) { auto overlayable_first = std::make_shared<Overlayable>("CustomizableResources", "overlay://customization"); OverlayableItem overlayable_item_first(overlayable_first); - overlayable_item_first.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_first.policies |= PolicyFlags::PRODUCT_PARTITION; std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -579,7 +572,7 @@ TEST_F(TableMergerTest, SameResourceDifferentNameFail) { auto overlayable_second = std::make_shared<Overlayable>("ThemeResources", "overlay://customization"); OverlayableItem overlayable_item_second(overlayable_second); - overlayable_item_second.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_second.policies |= PolicyFlags::PRODUCT_PARTITION; std::unique_ptr<ResourceTable> table_b = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -598,7 +591,7 @@ TEST_F(TableMergerTest, SameResourceDifferentActorFail) { auto overlayable_first = std::make_shared<Overlayable>("CustomizableResources", "overlay://customization"); OverlayableItem overlayable_item_first(overlayable_first); - overlayable_item_first.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_first.policies |= PolicyFlags::PRODUCT_PARTITION; std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -608,7 +601,7 @@ TEST_F(TableMergerTest, SameResourceDifferentActorFail) { auto overlayable_second = std::make_shared<Overlayable>("CustomizableResources", "overlay://theme"); OverlayableItem overlayable_item_second(overlayable_second); - overlayable_item_second.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_second.policies |= PolicyFlags::PRODUCT_PARTITION; std::unique_ptr<ResourceTable> table_b = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -627,7 +620,7 @@ TEST_F(TableMergerTest, SameResourceDifferentPoliciesFail) { auto overlayable_first = std::make_shared<Overlayable>("CustomizableResources", "overlay://customization"); OverlayableItem overlayable_item_first(overlayable_first); - overlayable_item_first.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_first.policies |= PolicyFlags::PRODUCT_PARTITION; std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -637,7 +630,7 @@ TEST_F(TableMergerTest, SameResourceDifferentPoliciesFail) { auto overlayable_second = std::make_shared<Overlayable>("CustomizableResources", "overlay://customization"); OverlayableItem overlayable_item_second(overlayable_second); - overlayable_item_second.policies |= OverlayableItem::Policy::kSignature; + overlayable_item_second.policies |= PolicyFlags::SIGNATURE; std::unique_ptr<ResourceTable> table_b = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -657,7 +650,7 @@ TEST_F(TableMergerTest, SameResourceSameOverlayable) { "overlay://customization"); OverlayableItem overlayable_item_first(overlayable); - overlayable_item_first.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_first.policies |= PolicyFlags::PRODUCT_PARTITION; std::unique_ptr<ResourceTable> table_a = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) @@ -665,7 +658,7 @@ TEST_F(TableMergerTest, SameResourceSameOverlayable) { .Build(); OverlayableItem overlayable_item_second(overlayable); - overlayable_item_second.policies |= OverlayableItem::Policy::kProduct; + overlayable_item_second.policies |= PolicyFlags::PRODUCT_PARTITION; std::unique_ptr<ResourceTable> table_b = test::ResourceTableBuilder() .SetPackageId("com.app.a", 0x7f) diff --git a/tools/aapt2/link/XmlReferenceLinker.cpp b/tools/aapt2/link/XmlReferenceLinker.cpp index d68f7dd44c9f..c3c16b92f712 100644 --- a/tools/aapt2/link/XmlReferenceLinker.cpp +++ b/tools/aapt2/link/XmlReferenceLinker.cpp @@ -83,6 +83,15 @@ class XmlVisitor : public xml::PackageAwareVisitor { Attribute default_attribute(android::ResTable_map::TYPE_ANY); default_attribute.SetWeak(true); + // The default orientation of gradients in android Q is different than previous android + // versions. Set the android:angle attribute to "0" to ensure that the default gradient + // orientation will remain left-to-right in android Q. + if (el->name == "gradient" && context_->GetMinSdkVersion() <= SDK_Q) { + if (!el->FindAttribute(xml::kSchemaAndroid, "angle")) { + el->attributes.push_back(xml::Attribute{xml::kSchemaAndroid, "angle", "0"}); + } + } + const Source source = source_.WithLine(el->line_number); for (xml::Attribute& attr : el->attributes) { // If the attribute has no namespace, interpret values as if @@ -99,7 +108,7 @@ class XmlVisitor : public xml::PackageAwareVisitor { std::string err_str; attr.compiled_attribute = - ReferenceLinker::CompileXmlAttribute(attr_ref, callsite_, symbols_, &err_str); + ReferenceLinker::CompileXmlAttribute(attr_ref, callsite_, context_, symbols_, &err_str); if (!attr.compiled_attribute) { DiagMessage error_msg(source); diff --git a/tools/aapt2/link/XmlReferenceLinker_test.cpp b/tools/aapt2/link/XmlReferenceLinker_test.cpp index ef99355e5b5f..0ce2e50d6e44 100644 --- a/tools/aapt2/link/XmlReferenceLinker_test.cpp +++ b/tools/aapt2/link/XmlReferenceLinker_test.cpp @@ -47,6 +47,8 @@ class XmlReferenceLinkerTest : public ::testing::Test { test::AttributeBuilder() .SetTypeMask(android::ResTable_map::TYPE_STRING) .Build()) + .AddPublicSymbol("android:attr/angle", ResourceId(0x01010004), + test::AttributeBuilder().Build()) // Add one real symbol that was introduces in v21 .AddPublicSymbol("android:attr/colorAccent", ResourceId(0x01010435), @@ -75,7 +77,7 @@ class XmlReferenceLinkerTest : public ::testing::Test { } protected: - std::unique_ptr<IAaptContext> context_; + std::unique_ptr<test::Context> context_; }; TEST_F(XmlReferenceLinkerTest, LinkBasicAttributes) { @@ -254,4 +256,63 @@ TEST_F(XmlReferenceLinkerTest, LinkViewWithLocalPackageAndAliasOfTheSameName) { EXPECT_EQ(make_value(ResourceId(0x7f030000)), ref->id); } + +TEST_F(XmlReferenceLinkerTest, AddAngleOnGradientForAndroidQ) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <gradient />)"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.Consume(context_.get(), doc.get())); + + xml::Element* gradient_el = doc->root.get(); + ASSERT_THAT(gradient_el, NotNull()); + + xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle"); + ASSERT_THAT(xml_attr, NotNull()); + ASSERT_TRUE(xml_attr->compiled_attribute); + EXPECT_EQ(make_value(ResourceId(0x01010004)), xml_attr->compiled_attribute.value().id); + + BinaryPrimitive* value = ValueCast<BinaryPrimitive>(xml_attr->compiled_value.get()); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(value->value.dataType, android::Res_value::TYPE_INT_DEC); + EXPECT_EQ(value->value.data, 0U); +} + +TEST_F(XmlReferenceLinkerTest, DoNotOverwriteAngleOnGradientForAndroidQ) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <gradient xmlns:android="http://schemas.android.com/apk/res/android" + android:angle="90"/>)"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.Consume(context_.get(), doc.get())); + + xml::Element* gradient_el = doc->root.get(); + ASSERT_THAT(gradient_el, NotNull()); + + xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle"); + ASSERT_THAT(xml_attr, NotNull()); + ASSERT_TRUE(xml_attr->compiled_attribute); + EXPECT_EQ(make_value(ResourceId(0x01010004)), xml_attr->compiled_attribute.value().id); + + BinaryPrimitive* value = ValueCast<BinaryPrimitive>(xml_attr->compiled_value.get()); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(value->value.dataType, android::Res_value::TYPE_INT_DEC); + EXPECT_EQ(value->value.data, 90U); +} + +TEST_F(XmlReferenceLinkerTest, DoNotOverwriteAngleOnGradientForPostAndroidQ) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"( + <gradient xmlns:android="http://schemas.android.com/apk/res/android" />)"); + context_->SetMinSdkVersion(30); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.Consume(context_.get(), doc.get())); + + xml::Element* gradient_el = doc->root.get(); + ASSERT_THAT(gradient_el, NotNull()); + + xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle"); + ASSERT_THAT(xml_attr, IsNull()); +} + } // namespace aapt diff --git a/tools/aapt2/optimize/MultiApkGenerator.cpp b/tools/aapt2/optimize/MultiApkGenerator.cpp index 8c9c43409569..c686a10a3fa9 100644 --- a/tools/aapt2/optimize/MultiApkGenerator.cpp +++ b/tools/aapt2/optimize/MultiApkGenerator.cpp @@ -101,6 +101,10 @@ class ContextWrapper : public IAaptContext { util::make_unique<SourcePathDiagnostics>(Source{source}, context_->GetDiagnostics()); } + const std::set<std::string>& GetSplitNameDependencies() override { + return context_->GetSplitNameDependencies(); + } + private: IAaptContext* context_; std::unique_ptr<SourcePathDiagnostics> source_diag_; diff --git a/tools/aapt2/optimize/ResourceDeduper.cpp b/tools/aapt2/optimize/ResourceDeduper.cpp index 78ebcb97b811..0278b439cfae 100644 --- a/tools/aapt2/optimize/ResourceDeduper.cpp +++ b/tools/aapt2/optimize/ResourceDeduper.cpp @@ -63,13 +63,14 @@ class DominatedKeyValueRemover : public DominatorTree::BottomUpVisitor { // Compare compatible configs for this entry and ensure the values are // equivalent. const ConfigDescription& node_configuration = node_value->config; - for (const auto& sibling : entry_->values) { - if (!sibling->value) { + for (const auto& sibling : parent->children()) { + ResourceConfigValue* sibling_value = sibling->value(); + if (!sibling_value->value) { // Sibling was already removed. continue; } - if (node_configuration.IsCompatibleWith(sibling->config) && - !node_value->value->Equals(sibling->value.get())) { + if (node_configuration.IsCompatibleWith(sibling_value->config) && + !node_value->value->Equals(sibling_value->value.get())) { // The configurations are compatible, but the value is // different, so we can't remove this value. return; diff --git a/tools/aapt2/optimize/ResourceDeduper_test.cpp b/tools/aapt2/optimize/ResourceDeduper_test.cpp index 2e098aec4f8d..048e318d2802 100644 --- a/tools/aapt2/optimize/ResourceDeduper_test.cpp +++ b/tools/aapt2/optimize/ResourceDeduper_test.cpp @@ -80,11 +80,58 @@ TEST(ResourceDeduperTest, DifferentValuesAreKept) { .Build(); ASSERT_TRUE(ResourceDeduper().Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/keep", default_config)); EXPECT_THAT(table, HasValue("android:string/keep", ldrtl_config)); EXPECT_THAT(table, HasValue("android:string/keep", ldrtl_v21_config)); EXPECT_THAT(table, HasValue("android:string/keep", land_config)); } +TEST(ResourceDeduperTest, SameValuesAreDedupedIncompatibleSiblings) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + const ConfigDescription ldrtl_config = test::ParseConfigOrDie("ldrtl"); + const ConfigDescription ldrtl_night_config = test::ParseConfigOrDie("ldrtl-night"); + // Chosen because this configuration is not compatible with ldrtl-night. + const ConfigDescription ldrtl_notnight_config = test::ParseConfigOrDie("ldrtl-notnight"); + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddString("android:string/keep", ResourceId{}, default_config, "keep") + .AddString("android:string/keep", ResourceId{}, ldrtl_config, "dedupe") + .AddString("android:string/keep", ResourceId{}, ldrtl_night_config, "dedupe") + .AddString("android:string/keep", ResourceId{}, ldrtl_notnight_config, "keep2") + .Build(); + + ASSERT_TRUE(ResourceDeduper().Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/keep", default_config)); + EXPECT_THAT(table, HasValue("android:string/keep", ldrtl_config)); + EXPECT_THAT(table, Not(HasValue("android:string/keep", ldrtl_night_config))); + EXPECT_THAT(table, HasValue("android:string/keep", ldrtl_notnight_config)); +} + +TEST(ResourceDeduperTest, SameValuesAreDedupedCompatibleNonSiblings) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const ConfigDescription default_config = {}; + const ConfigDescription ldrtl_config = test::ParseConfigOrDie("ldrtl"); + const ConfigDescription ldrtl_night_config = test::ParseConfigOrDie("ldrtl-night"); + // Chosen because this configuration is compatible with ldrtl. + const ConfigDescription land_config = test::ParseConfigOrDie("land"); + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddString("android:string/keep", ResourceId{}, default_config, "keep") + .AddString("android:string/keep", ResourceId{}, ldrtl_config, "dedupe") + .AddString("android:string/keep", ResourceId{}, ldrtl_night_config, "dedupe") + .AddString("android:string/keep", ResourceId{}, land_config, "keep2") + .Build(); + + ASSERT_TRUE(ResourceDeduper().Consume(context.get(), table.get())); + EXPECT_THAT(table, HasValue("android:string/keep", default_config)); + EXPECT_THAT(table, HasValue("android:string/keep", ldrtl_config)); + EXPECT_THAT(table, Not(HasValue("android:string/keep", ldrtl_night_config))); + EXPECT_THAT(table, HasValue("android:string/keep", land_config)); +} + TEST(ResourceDeduperTest, LocalesValuesAreKept) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); const ConfigDescription default_config = {}; diff --git a/tools/aapt2/optimize/ResourcePathShortener.cpp b/tools/aapt2/optimize/ResourcePathShortener.cpp index c5df3dd00db9..7ff9bf5aa8df 100644 --- a/tools/aapt2/optimize/ResourcePathShortener.cpp +++ b/tools/aapt2/optimize/ResourcePathShortener.cpp @@ -16,13 +16,14 @@ #include "optimize/ResourcePathShortener.h" -#include <math.h> +#include <set> #include <unordered_set> #include "androidfw/StringPiece.h" #include "ResourceTable.h" #include "ValueVisitor.h" +#include "util/Util.h" static const std::string base64_chars = @@ -50,18 +51,15 @@ std::string ShortenFileName(const android::StringPiece& file_path, int output_le } -// Calculate the optimal hash length such that an average of 10% of resources -// collide in their shortened path. +// Return the optimal hash length such that at most 10% of resources collide in +// their shortened path. // Reference: http://matt.might.net/articles/counting-hash-collisions/ int OptimalShortenedLength(int num_resources) { - int num_chars = 2; - double N = 64*64; // hash space when hash is 2 chars long - double max_collisions = num_resources * 0.1; - while (num_resources - N + N * pow((N - 1) / N, num_resources) > max_collisions) { - N *= 64; - num_chars++; + if (num_resources > 4000) { + return 3; + } else { + return 2; } - return num_chars; } std::string GetShortenedPath(const android::StringPiece& shortened_filename, @@ -74,10 +72,19 @@ std::string GetShortenedPath(const android::StringPiece& shortened_filename, return shortened_path; } +// implement custom comparator of FileReference pointers so as to use the +// underlying filepath as key rather than the integer address. This is to ensure +// determinism of output for colliding files. +struct PathComparator { + bool operator() (const FileReference* lhs, const FileReference* rhs) const { + return lhs->path->compare(*rhs->path); + } +}; + bool ResourcePathShortener::Consume(IAaptContext* context, ResourceTable* table) { // used to detect collisions std::unordered_set<std::string> shortened_paths; - std::unordered_set<FileReference*> file_refs; + std::set<FileReference*, PathComparator> file_refs; for (auto& package : table->packages) { for (auto& type : package->types) { for (auto& entry : type->entries) { @@ -95,6 +102,10 @@ bool ResourcePathShortener::Consume(IAaptContext* context, ResourceTable* table) android::StringPiece res_subdir, actual_filename, extension; util::ExtractResFilePathParts(*file_ref->path, &res_subdir, &actual_filename, &extension); + // Android detects ColorStateLists via pathname, skip res/color* + if (util::StartsWith(res_subdir, "res/color")) + continue; + std::string shortened_filename = ShortenFileName(*file_ref->path, num_chars); int collision_count = 0; std::string shortened_path = GetShortenedPath(shortened_filename, extension, collision_count); diff --git a/tools/aapt2/optimize/ResourcePathShortener_test.cpp b/tools/aapt2/optimize/ResourcePathShortener_test.cpp index 88cadc76c336..f5a02be0ea5e 100644 --- a/tools/aapt2/optimize/ResourcePathShortener_test.cpp +++ b/tools/aapt2/optimize/ResourcePathShortener_test.cpp @@ -24,6 +24,19 @@ using ::testing::Not; using ::testing::NotNull; using ::testing::Eq; +android::StringPiece GetExtension(android::StringPiece path) { + auto iter = std::find(path.begin(), path.end(), '.'); + return android::StringPiece(iter, path.end() - iter); +} + +void FillTable(aapt::test::ResourceTableBuilder& builder, int start, int end) { + for (int i=start; i<end; i++) { + builder.AddFileReference( + "android:drawable/xmlfile" + std::to_string(i), + "res/drawable/xmlfile" + std::to_string(i) + ".xml"); + } +} + namespace aapt { TEST(ResourcePathShortenerTest, FileRefPathsChangedInResourceTable) { @@ -64,4 +77,90 @@ TEST(ResourcePathShortenerTest, FileRefPathsChangedInResourceTable) { EXPECT_THAT(path_map.find("res/should/still/be/the/same.png"), Eq(path_map.end())); } +TEST(ResourcePathShortenerTest, SkipColorFileRefPaths) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddFileReference("android:color/colorlist", "res/color/colorlist.xml") + .AddFileReference("android:color/colorlist", + "res/color-mdp-v21/colorlist.xml", + test::ParseConfigOrDie("mdp-v21")) + .Build(); + + std::map<std::string, std::string> path_map; + ASSERT_TRUE(ResourcePathShortener(path_map).Consume(context.get(), table.get())); + + // Expect that the path map to not contain the ColorStateList + ASSERT_THAT(path_map.find("res/color/colorlist.xml"), Eq(path_map.end())); + ASSERT_THAT(path_map.find("res/color-mdp-v21/colorlist.xml"), Eq(path_map.end())); +} + +TEST(ResourcePathShortenerTest, KeepExtensions) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + std::string original_xml_path = "res/drawable/xmlfile.xml"; + std::string original_png_path = "res/drawable/pngfile.png"; + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddFileReference("android:color/xmlfile", original_xml_path) + .AddFileReference("android:color/pngfile", original_png_path) + .Build(); + + std::map<std::string, std::string> path_map; + ASSERT_TRUE(ResourcePathShortener(path_map).Consume(context.get(), table.get())); + + // Expect that the path map is populated + ASSERT_THAT(path_map.find("res/drawable/xmlfile.xml"), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find("res/drawable/pngfile.png"), Not(Eq(path_map.end()))); + + auto shortend_xml_path = path_map[original_xml_path]; + auto shortend_png_path = path_map[original_png_path]; + + EXPECT_THAT(GetExtension(path_map[original_xml_path]), Eq(android::StringPiece(".xml"))); + EXPECT_THAT(GetExtension(path_map[original_png_path]), Eq(android::StringPiece(".png"))); +} + +TEST(ResourcePathShortenerTest, DeterministicallyHandleCollisions) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + // 4000 resources is the limit at which the hash space is expanded to 3 + // letters to reduce collisions, we want as many collisions as possible thus + // N-1. + const auto kNumResources = 3999; + const auto kNumTries = 5; + + test::ResourceTableBuilder builder1; + FillTable(builder1, 0, kNumResources); + std::unique_ptr<ResourceTable> table1 = builder1.Build(); + std::map<std::string, std::string> expected_mapping; + ASSERT_TRUE(ResourcePathShortener(expected_mapping).Consume(context.get(), table1.get())); + + // We are trying to ensure lack of non-determinism, it is not simple to prove + // a negative, thus we must try the test a few times so that the test itself + // is non-flaky. Basically create the pathmap 5 times from the same set of + // resources but a different order of addition and then ensure they are always + // mapped to the same short path. + for (int i=0; i<kNumTries; i++) { + test::ResourceTableBuilder builder2; + // This loop adds resources to the resource table in the range of + // [0:kNumResources). Adding the file references in different order makes + // non-determinism more likely to surface. Thus we add resources + // [start_index:kNumResources) first then [0:start_index). We also use a + // different start_index each run. + int start_index = (kNumResources/kNumTries)*i; + FillTable(builder2, start_index, kNumResources); + FillTable(builder2, 0, start_index); + std::unique_ptr<ResourceTable> table2 = builder2.Build(); + + std::map<std::string, std::string> actual_mapping; + ASSERT_TRUE(ResourcePathShortener(actual_mapping).Consume(context.get(), table2.get())); + + for (auto& item : actual_mapping) { + ASSERT_THAT(expected_mapping[item.first], Eq(item.second)); + } + } +} + } // namespace aapt diff --git a/tools/aapt2/process/IResourceTableConsumer.h b/tools/aapt2/process/IResourceTableConsumer.h index 30dad8025900..9c4b323db433 100644 --- a/tools/aapt2/process/IResourceTableConsumer.h +++ b/tools/aapt2/process/IResourceTableConsumer.h @@ -19,6 +19,7 @@ #include <iostream> #include <list> +#include <set> #include <sstream> #include "Diagnostics.h" @@ -50,6 +51,7 @@ struct IAaptContext { virtual NameMangler* GetNameMangler() = 0; virtual bool IsVerbose() = 0; virtual int GetMinSdkVersion() = 0; + virtual const std::set<std::string>& GetSplitNameDependencies() = 0; }; struct IResourceTableConsumer { diff --git a/tools/aapt2/process/SymbolTable.cpp b/tools/aapt2/process/SymbolTable.cpp index 61a8fbbb7f52..897fa80ffedb 100644 --- a/tools/aapt2/process/SymbolTable.cpp +++ b/tools/aapt2/process/SymbolTable.cpp @@ -245,7 +245,8 @@ std::map<size_t, std::string> AssetManagerSymbolSource::GetAssignedPackageIds() return package_map; } -bool AssetManagerSymbolSource::IsPackageDynamic(uint32_t packageId) const { +bool AssetManagerSymbolSource::IsPackageDynamic(uint32_t packageId, + const std::string& package_name) const { if (packageId == 0) { return true; } @@ -253,7 +254,7 @@ bool AssetManagerSymbolSource::IsPackageDynamic(uint32_t packageId) const { for (const std::unique_ptr<const ApkAssets>& assets : apk_assets_) { for (const std::unique_ptr<const android::LoadedPackage>& loaded_package : assets->GetLoadedArsc()->GetPackages()) { - if (packageId == loaded_package->GetPackageId() && loaded_package->IsDynamic()) { + if (package_name == loaded_package->GetPackageName() && loaded_package->IsDynamic()) { return true; } } @@ -313,6 +314,7 @@ static std::unique_ptr<SymbolTable::Symbol> LookupAttributeInTable( symbol.symbol.name = parsed_name.value(); symbol.symbol.id = ResourceId(map_entry.key); symbol.value = map_entry.value.data; + symbol.type = map_entry.value.dataType; s->attribute->symbols.push_back(std::move(symbol)); } } @@ -327,19 +329,19 @@ std::unique_ptr<SymbolTable::Symbol> AssetManagerSymbolSource::FindByName( bool found = false; ResourceId res_id = 0; uint32_t type_spec_flags; + ResourceName real_name; // There can be mangled resources embedded within other packages. Here we will // look into each package and look-up the mangled name until we find the resource. asset_manager_.ForEachPackage([&](const std::string& package_name, uint8_t id) -> bool { - ResourceName real_name(name.package, name.type, name.entry); - + real_name = ResourceName(name.package, name.type, name.entry); if (package_name != name.package) { real_name.entry = mangled_entry; real_name.package = package_name; } res_id = asset_manager_.GetResourceId(real_name.to_string()); - if (res_id.is_valid() && asset_manager_.GetResourceFlags(res_id.id, &type_spec_flags)) { + if (res_id.is_valid_static() && asset_manager_.GetResourceFlags(res_id.id, &type_spec_flags)) { found = true; return false; } @@ -352,12 +354,12 @@ std::unique_ptr<SymbolTable::Symbol> AssetManagerSymbolSource::FindByName( } std::unique_ptr<SymbolTable::Symbol> s; - if (name.type == ResourceType::kAttr) { + if (real_name.type == ResourceType::kAttr) { s = LookupAttributeInTable(asset_manager_, res_id); } else { s = util::make_unique<SymbolTable::Symbol>(); s->id = res_id; - s->is_dynamic = IsPackageDynamic(ResourceId(res_id).package_id()); + s->is_dynamic = IsPackageDynamic(ResourceId(res_id).package_id(), real_name.package); } if (s) { @@ -378,7 +380,7 @@ static Maybe<ResourceName> GetResourceName(android::AssetManager2& am, std::unique_ptr<SymbolTable::Symbol> AssetManagerSymbolSource::FindById( ResourceId id) { - if (!id.is_valid()) { + if (!id.is_valid_static()) { // Exit early and avoid the error logs from AssetManager. return {}; } @@ -405,7 +407,7 @@ std::unique_ptr<SymbolTable::Symbol> AssetManagerSymbolSource::FindById( } else { s = util::make_unique<SymbolTable::Symbol>(); s->id = id; - s->is_dynamic = IsPackageDynamic(ResourceId(id).package_id()); + s->is_dynamic = IsPackageDynamic(ResourceId(id).package_id(), name.package); } if (s) { diff --git a/tools/aapt2/process/SymbolTable.h b/tools/aapt2/process/SymbolTable.h index 6997cd6714a8..06eaf63ad442 100644 --- a/tools/aapt2/process/SymbolTable.h +++ b/tools/aapt2/process/SymbolTable.h @@ -194,7 +194,7 @@ class AssetManagerSymbolSource : public ISymbolSource { bool AddAssetPath(const android::StringPiece& path); std::map<size_t, std::string> GetAssignedPackageIds() const; - bool IsPackageDynamic(uint32_t packageId) const; + bool IsPackageDynamic(uint32_t packageId, const std::string& package_name) const; std::unique_ptr<SymbolTable::Symbol> FindByName( const ResourceName& name) override; diff --git a/tools/aapt2/test/Context.h b/tools/aapt2/test/Context.h index 0564db063b9a..553c43e6c469 100644 --- a/tools/aapt2/test/Context.h +++ b/tools/aapt2/test/Context.h @@ -81,6 +81,14 @@ class Context : public IAaptContext { return min_sdk_version_; } + void SetMinSdkVersion(int min_sdk_version) { + min_sdk_version_ = min_sdk_version; + } + + const std::set<std::string>& GetSplitNameDependencies() override { + return split_name_dependencies_; + } + private: DISALLOW_COPY_AND_ASSIGN(Context); @@ -93,6 +101,7 @@ class Context : public IAaptContext { NameMangler name_mangler_; SymbolTable symbols_; int min_sdk_version_; + std::set<std::string> split_name_dependencies_; }; class ContextBuilder { @@ -127,6 +136,11 @@ class ContextBuilder { return *this; } + ContextBuilder& SetSplitNameDependencies(const std::set<std::string>& split_name_dependencies) { + context_->split_name_dependencies_ = split_name_dependencies; + return *this; + } + std::unique_ptr<Context> Build() { return std::move(context_); } private: diff --git a/tools/aapt2/test/Fixture.cpp b/tools/aapt2/test/Fixture.cpp index a51b4a4649f1..5386802dbc8e 100644 --- a/tools/aapt2/test/Fixture.cpp +++ b/tools/aapt2/test/Fixture.cpp @@ -80,7 +80,7 @@ void TestDirectoryFixture::TearDown() { ClearDirectory(temp_dir_); } -bool TestDirectoryFixture::WriteFile(const std::string& path, const std::string& contents) { +void TestDirectoryFixture::WriteFile(const std::string& path, const std::string& contents) { CHECK(util::StartsWith(path, temp_dir_)) << "Attempting to create a file outside of test temporary directory."; @@ -91,16 +91,31 @@ bool TestDirectoryFixture::WriteFile(const std::string& path, const std::string& file::mkdirs(dirs); } - return android::base::WriteStringToFile(contents, path); + CHECK(android::base::WriteStringToFile(contents, path)); } bool CommandTestFixture::CompileFile(const std::string& path, const std::string& contents, const android::StringPiece& out_dir, IDiagnostics* diag) { - CHECK(WriteFile(path, contents)); + WriteFile(path, contents); CHECK(file::mkdirs(out_dir.data())); return CompileCommand(diag).Execute({path, "-o", out_dir, "-v"}, &std::cerr) == 0; } +bool CommandTestFixture::Link(const std::vector<std::string>& args, IDiagnostics* diag) { + std::vector<android::StringPiece> link_args; + for(const std::string& arg : args) { + link_args.emplace_back(arg); + } + + // Link against the android SDK + std::string android_sdk = file::BuildPath({android::base::GetExecutableDirectory(), + "integration-tests", "CommandTests", + "android-28.jar"}); + link_args.insert(link_args.end(), {"-I", android_sdk}); + + return LinkCommand(diag).Execute(link_args, &std::cerr) == 0; +} + bool CommandTestFixture::Link(const std::vector<std::string>& args, const android::StringPiece& flat_dir, IDiagnostics* diag) { std::vector<android::StringPiece> link_args; @@ -128,10 +143,10 @@ bool CommandTestFixture::Link(const std::vector<std::string>& args, std::string CommandTestFixture::GetDefaultManifest(const char* package_name) { const std::string manifest_file = GetTestPath("AndroidManifest.xml"); - CHECK(WriteFile(manifest_file, android::base::StringPrintf(R"( + WriteFile(manifest_file, android::base::StringPrintf(R"( <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="%s"> - </manifest>)", package_name))); + </manifest>)", package_name)); return manifest_file; } diff --git a/tools/aapt2/test/Fixture.h b/tools/aapt2/test/Fixture.h index fce2aebfecaa..457d65e30b65 100644 --- a/tools/aapt2/test/Fixture.h +++ b/tools/aapt2/test/Fixture.h @@ -58,7 +58,7 @@ class TestDirectoryFixture : public ::testing::Test { // Creates a file with the specified contents, creates any intermediate directories in the // process. The file path must be an absolute path within the test directory. - bool WriteFile(const std::string& path, const std::string& contents); + void WriteFile(const std::string& path, const std::string& contents); private: std::string temp_dir_; @@ -75,6 +75,9 @@ class CommandTestFixture : public TestDirectoryFixture { bool CompileFile(const std::string& path, const std::string& contents, const android::StringPiece& flat_out_dir, IDiagnostics* diag); + // Executes the link command with the specified arguments. + bool Link(const std::vector<std::string>& args, IDiagnostics* diag); + // Executes the link command with the specified arguments. The flattened files residing in the // flat directory will be added to the link command as file arguments. bool Link(const std::vector<std::string>& args, const android::StringPiece& flat_dir, diff --git a/tools/bit/main.cpp b/tools/bit/main.cpp index d80c2e742fae..fd184f50091a 100644 --- a/tools/bit/main.cpp +++ b/tools/bit/main.cpp @@ -708,10 +708,12 @@ run_phases(vector<Target*> targets, const Options& options) } } + // Figure out whether we need to sync the system and which apks to install string deviceTargetPath = buildOut + "/target/product/" + buildDevice; string systemPath = deviceTargetPath + "/system/"; string dataPath = deviceTargetPath + "/data/"; + string testPath = deviceTargetPath + "/testcases/"; bool syncSystem = false; bool alwaysSyncSystem = false; vector<string> systemFiles; @@ -734,7 +736,8 @@ run_phases(vector<Target*> targets, const Options& options) continue; } // Apk in the data partition - if (starts_with(file, dataPath) && ends_with(file, ".apk")) { + if (ends_with(file, ".apk") + && (starts_with(file, dataPath) || starts_with(file, testPath))) { // Always install it if we didn't build it because otherwise // it will never have changed. installApks.push_back(InstallApk(file, !target->build)); @@ -966,8 +969,9 @@ run_phases(vector<Target*> targets, const Options& options) for (size_t j=0; j<target->module.installed.size(); j++) { string filename = target->module.installed[j]; - // Apk in the data partition - if (!starts_with(filename, dataPath) || !ends_with(filename, ".apk")) { + // Skip of not apk in the data partition or test + if (!(ends_with(filename, ".apk") + && (starts_with(filename, dataPath) || starts_with(filename, testPath)))) { continue; } diff --git a/tools/codegen/.gitignore b/tools/codegen/.gitignore new file mode 100755 index 000000000000..9fb18b42668f --- /dev/null +++ b/tools/codegen/.gitignore @@ -0,0 +1,2 @@ +.idea +out diff --git a/tools/codegen/Android.bp b/tools/codegen/Android.bp new file mode 100644 index 000000000000..677bee2cce81 --- /dev/null +++ b/tools/codegen/Android.bp @@ -0,0 +1,18 @@ +java_binary_host { + name: "codegen_cli", + manifest: "manifest.txt", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "javaparser", + ], +} + +java_library_host { + name: "codegen-version-info", + + srcs: [ + "src/**/SharedConstants.kt", + ], +} diff --git a/tools/codegen/OWNERS b/tools/codegen/OWNERS new file mode 100644 index 000000000000..da723b3b67da --- /dev/null +++ b/tools/codegen/OWNERS @@ -0,0 +1 @@ +eugenesusla@google.com
\ No newline at end of file diff --git a/tools/codegen/manifest.txt b/tools/codegen/manifest.txt new file mode 100644 index 000000000000..6e1018ba6b55 --- /dev/null +++ b/tools/codegen/manifest.txt @@ -0,0 +1 @@ +Main-class: com.android.codegen.MainKt diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt new file mode 100644 index 000000000000..bf95a2eb2193 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt @@ -0,0 +1,27 @@ +package com.android.codegen + +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration + +open class ClassInfo(val classAst: ClassOrInterfaceDeclaration, val fileInfo: FileInfo) { + + val fileAst = fileInfo.fileAst + + val nestedClasses = classAst.members.filterIsInstance<ClassOrInterfaceDeclaration>() + + val superInterfaces = classAst.implementedTypes.map { it.asString() } + val superClass = classAst.extendedTypes.getOrNull(0) + + val ClassName = classAst.nameAsString + private val genericArgsAst = classAst.typeParameters + val genericArgs = if (genericArgsAst.isEmpty()) "" else { + genericArgsAst.map { it.nameAsString }.joinToString(", ").let { "<$it>" } + } + val ClassType = ClassName + genericArgs + + val constDefs = mutableListOf<ConstDef>() + + val fields = classAst.fields + .filterNot { it.isTransient || it.isStatic } + .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) } + .apply { lastOrNull()?.isLast = true } +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/ClassPrinter.kt b/tools/codegen/src/com/android/codegen/ClassPrinter.kt new file mode 100644 index 000000000000..b90e1bb3e7e7 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt @@ -0,0 +1,234 @@ +package com.android.codegen + +import com.github.javaparser.ast.Modifier +import com.github.javaparser.ast.body.CallableDeclaration +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.body.TypeDeclaration +import com.github.javaparser.ast.expr.* +import com.github.javaparser.ast.type.ClassOrInterfaceType + +/** + * [ClassInfo] + utilities for printing out new class code with proper indentation and imports + */ +class ClassPrinter( + classAst: ClassOrInterfaceDeclaration, + fileInfo: FileInfo +) : ClassInfo(classAst, fileInfo), Printer<ClassPrinter>, ImportsProvider { + + val GENERATED_MEMBER_HEADER by lazy { "@$GeneratedMember" } + + init { + val fieldsWithMissingNullablity = fields.filter { field -> + !field.isPrimitive + && field.fieldAst.modifiers.none { it.keyword == Modifier.Keyword.TRANSIENT } + && "@$Nullable" !in field.annotations + && "@$NonNull" !in field.annotations + } + if (fieldsWithMissingNullablity.isNotEmpty()) { + abort("Non-primitive fields must have @$Nullable or @$NonNull annotation.\n" + + "Missing nullability annotations on: " + + fieldsWithMissingNullablity.joinToString(", ") { it.name }) + } + + if (!classAst.isFinal && + classAst.extendedTypes.any { it.nameAsString == Parcelable }) { + abort("Parcelable classes must be final") + } + } + + val cliArgs get() = fileInfo.cliArgs + + fun print() { + currentIndent = fileInfo.sourceLines + .find { "class $ClassName" in it }!! + .takeWhile { it.isWhitespace() } + .plus(INDENT_SINGLE) + + +fileInfo.generatedWarning + + if (FeatureFlag.CONST_DEFS()) generateConstDefs() + + + if (FeatureFlag.CONSTRUCTOR()) { + generateConstructor("public") + } else if (FeatureFlag.BUILDER() + || FeatureFlag.COPY_CONSTRUCTOR() + || FeatureFlag.WITHERS()) { + generateConstructor("/* package-private */") + } + if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor() + + if (FeatureFlag.GETTERS()) generateGetters() + if (FeatureFlag.SETTERS()) generateSetters() + if (FeatureFlag.TO_STRING()) generateToString() + if (FeatureFlag.EQUALS_HASH_CODE()) generateEqualsHashcode() + + if (FeatureFlag.FOR_EACH_FIELD()) generateForEachField() + + if (FeatureFlag.WITHERS()) generateWithers() + + if (FeatureFlag.PARCELABLE()) generateParcelable() + + if (FeatureFlag.BUILDER() && FeatureFlag.BUILD_UPON()) generateBuildUpon() + if (FeatureFlag.BUILDER()) generateBuilder() + + if (FeatureFlag.AIDL()) fileInfo.generateAidl() //TODO guard against nested classes requesting aidl + + generateMetadata(fileInfo.file) + + +""" + //@formatter:on + $GENERATED_END + + """ + + rmEmptyLine() + } + + override var currentIndent: String + get() = fileInfo.currentIndent + set(value) { fileInfo.currentIndent = value } + override val stringBuilder get() = fileInfo.stringBuilder + + + val dataClassAnnotationFeatures = classAst.annotations + .find { it.nameAsString == DataClass } + ?.let { it as? NormalAnnotationExpr } + ?.pairs + ?.map { pair -> pair.nameAsString to (pair.value as BooleanLiteralExpr).value } + ?.toMap() + ?: emptyMap() + + val internalAnnotations = setOf(ParcelWith, DataClassEnum, PluralOf, UnsupportedAppUsage, + DataClassSuppressConstDefs, MaySetToNull, Each, DataClass) + val knownNonValidationAnnotations = internalAnnotations + Each + Nullable + + /** + * @return whether the given feature is enabled + */ + operator fun FeatureFlag.invoke(): Boolean { + if (cliArgs.contains("--no-$kebabCase")) return false + if (cliArgs.contains("--$kebabCase")) return true + + val annotationKey = "gen$upperCamelCase" + val annotationHiddenKey = "genHidden$upperCamelCase" + if (dataClassAnnotationFeatures.containsKey(annotationKey)) { + return dataClassAnnotationFeatures[annotationKey]!! + } + if (dataClassAnnotationFeatures.containsKey(annotationHiddenKey)) { + return dataClassAnnotationFeatures[annotationHiddenKey]!! + } + + if (cliArgs.contains("--all")) return true + if (hidden) return true + + return when (this) { + FeatureFlag.SETTERS -> + !FeatureFlag.CONSTRUCTOR() && !FeatureFlag.BUILDER() && fields.any { !it.isFinal } + FeatureFlag.BUILDER -> cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS) + || fields.any { it.hasDefault } + || onByDefault + FeatureFlag.CONSTRUCTOR -> !FeatureFlag.BUILDER() + FeatureFlag.PARCELABLE -> "Parcelable" in superInterfaces + FeatureFlag.AIDL -> fileInfo.mainClass.nameAsString == ClassName && FeatureFlag.PARCELABLE() + FeatureFlag.IMPLICIT_NONNULL -> fields.any { it.isNullable } + && fields.none { "@$NonNull" in it.annotations } + else -> onByDefault + } + } + + val FeatureFlag.hidden: Boolean + get(): Boolean { + val annotationHiddenKey = "genHidden$upperCamelCase" + if (dataClassAnnotationFeatures.containsKey(annotationHiddenKey)) { + return dataClassAnnotationFeatures[annotationHiddenKey]!! + } + return when { + cliArgs.contains("--hidden-$kebabCase") -> true + this == FeatureFlag.BUILD_UPON -> FeatureFlag.BUILDER.hidden + else -> false + } + } + + + + inline operator fun <R> invoke(f: ClassPrinter.() -> R): R = run(f) + + var BuilderClass = CANONICAL_BUILDER_CLASS + var BuilderType = BuilderClass + genericArgs + val customBaseBuilderAst: ClassOrInterfaceDeclaration? by lazy { + nestedClasses.find { it.nameAsString == BASE_BUILDER_CLASS } + } + + val suppressedMembers by lazy { + getSuppressedMembers(classAst) + } + val builderSuppressedMembers by lazy { + getSuppressedMembers(customBaseBuilderAst) + suppressedMembers.mapNotNull { + if (it.startsWith("$CANONICAL_BUILDER_CLASS.")) { + it.removePrefix("$CANONICAL_BUILDER_CLASS.") + } else { + null + } + } + } + + private fun getSuppressedMembers(clazz: ClassOrInterfaceDeclaration?): List<String> { + return clazz + ?.annotations + ?.find { it.nameAsString == DataClassSuppress } + ?.as_<SingleMemberAnnotationExpr>() + ?.memberValue + ?.run { + when (this) { + is ArrayInitializerExpr -> values.map { it.asLiteralStringValueExpr().value } + is StringLiteralExpr -> listOf(value) + else -> abort("Can't parse annotation arg: $this") + } + } + ?: emptyList() + } + + fun isMethodGenerationSuppressed(name: String, vararg argTypes: String): Boolean { + return name in suppressedMembers || hasMethod(name, *argTypes) + } + + fun hasMethod(name: String, vararg argTypes: String): Boolean { + val members: List<CallableDeclaration<*>> = + if (name == ClassName) classAst.constructors else classAst.methods + return members.any { + it.name.asString() == name && + it.parameters.map { it.type.asString() } == argTypes.toList() + } + } + + val lazyTransientFields = classAst.fields + .filter { it.isTransient && !it.isStatic } + .mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) } + .filter { hasMethod("lazyInit${it.NameUpperCamel}") } + + val extendsParcelableClass by lazy { + Parcelable !in superInterfaces && superClass != null + } + + init { + val builderFactoryOverride = classAst.methods.find { + it.isStatic && it.nameAsString == "builder" + } + if (builderFactoryOverride != null) { + BuilderClass = (builderFactoryOverride.type as ClassOrInterfaceType).nameAsString + BuilderType = builderFactoryOverride.type.asString() + } else { + val builderExtension = classAst + .childNodes + .filterIsInstance(TypeDeclaration::class.java) + .find { it.nameAsString == CANONICAL_BUILDER_CLASS } + if (builderExtension != null) { + BuilderClass = BASE_BUILDER_CLASS + val tp = (builderExtension as ClassOrInterfaceDeclaration).typeParameters + BuilderType = if (tp.isEmpty()) BuilderClass + else "$BuilderClass<${tp.map { it.nameAsString }.joinToString(", ")}>" + } + } + } +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/ConstDef.kt b/tools/codegen/src/com/android/codegen/ConstDef.kt new file mode 100644 index 000000000000..f559d6f87027 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/ConstDef.kt @@ -0,0 +1,17 @@ +package com.android.codegen + +import com.github.javaparser.ast.body.FieldDeclaration + +/** + * `@IntDef` or `@StringDef` + */ +data class ConstDef(val type: Type, val AnnotationName: String, val values: List<FieldDeclaration>) { + + enum class Type { + INT, INT_FLAGS, STRING; + + val isInt get() = this == INT || this == INT_FLAGS + } + + val CONST_NAMES get() = values.flatMap { it.variables }.map { it.nameAsString } +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/FeatureFlag.kt b/tools/codegen/src/com/android/codegen/FeatureFlag.kt new file mode 100644 index 000000000000..24150d637a7b --- /dev/null +++ b/tools/codegen/src/com/android/codegen/FeatureFlag.kt @@ -0,0 +1,27 @@ +package com.android.codegen + + +/** + * See also [ClassPrinter.invoke] for more default flag values resolution rules + */ +enum class FeatureFlag(val onByDefault: Boolean, val desc: String = "") { + PARCELABLE(false, "implement Parcelable contract"), + AIDL(false, "generate a 'parcelable declaration' .aidl file alongside"), + CONSTRUCTOR(true, "an all-argument constructor"), + BUILDER(false, "e.g. MyClass.builder().setFoo(..).build();"), + GETTERS(true, "getters, e.g. getFoo()"), + SETTERS(false, "chainable/fluent setters, e.g. setFoo(..).setBar(..)"), + WITHERS(false, "'immutable setters' returning a new instance, " + + "e.g. newFoo = foo.withBar(barValue)"), + EQUALS_HASH_CODE(false, "equals + hashCode based on fields"), + TO_STRING(false, "toString based on fields"), + BUILD_UPON(false, "builder factory from existing instance, " + + "e.g. instance.buildUpon().setFoo(..).build()"), + IMPLICIT_NONNULL(true, "treat lack of @Nullable as @NonNull for Object fields"), + COPY_CONSTRUCTOR(false, "a constructor for an instance identical to the given one"), + CONST_DEFS(true, "@Int/StringDef's based on declared static constants"), + FOR_EACH_FIELD(false, "forEachField((name, value) -> ...)"); + + val kebabCase = name.toLowerCase().replace("_", "-") + val upperCamelCase = name.split("_").map { it.toLowerCase().capitalize() }.joinToString("") +} diff --git a/tools/codegen/src/com/android/codegen/FieldInfo.kt b/tools/codegen/src/com/android/codegen/FieldInfo.kt new file mode 100644 index 000000000000..02ebaef90f0b --- /dev/null +++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt @@ -0,0 +1,230 @@ +package com.android.codegen + +import com.github.javaparser.JavaParser +import com.github.javaparser.ast.body.FieldDeclaration +import com.github.javaparser.ast.expr.ClassExpr +import com.github.javaparser.ast.expr.Name +import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr +import com.github.javaparser.ast.expr.StringLiteralExpr +import com.github.javaparser.ast.type.ArrayType +import com.github.javaparser.ast.type.ClassOrInterfaceType +import com.github.javaparser.javadoc.Javadoc + +data class FieldInfo( + val index: Int, + val fieldAst: FieldDeclaration, + private val classInfo: ClassInfo +) { + + val classPrinter = classInfo as ClassPrinter + + // AST + internal val variableAst = fieldAst.variables[0] + val typeAst = variableAst.type + + // Field type + val Type = typeAst.asString() + val FieldClass = Type.takeWhile { it != '<' } + val isPrimitive = Type in PRIMITIVE_TYPES + + // Javadoc + val javadoc: Javadoc? = fieldAst.javadoc.orElse(null) + private val javadocText = javadoc?.toText()?.let { + // Workaround for a bug in Javaparser for javadocs starting with { + if (it.hasUnbalancedCurlyBrace()) "{$it" else it + } + val javadocTextNoAnnotationLines = javadocText + ?.lines() + ?.dropLastWhile { it.startsWith("@") || it.isBlank() } + ?.let { if (it.isEmpty()) null else it } + val javadocFull = javadocText + ?.trimBlankLines() + ?.mapLines { " * $this" } + ?.let { "/**\n$it\n */" } + + + // Field name + val name = variableAst.name.asString()!! + private val isNameHungarian = name[0] == 'm' && name[1].isUpperCase() + val NameUpperCamel = if (isNameHungarian) name.substring(1) else name.capitalize() + val nameLowerCamel = if (isNameHungarian) NameUpperCamel.decapitalize() else name + val _name = if (name != nameLowerCamel) nameLowerCamel else "_$nameLowerCamel" + val SingularNameOrNull by lazy { + classPrinter { + fieldAst.annotations + .find { it.nameAsString == PluralOf } + ?.let { it as? SingleMemberAnnotationExpr } + ?.memberValue + ?.let { it as? StringLiteralExpr } + ?.value + ?.toLowerCamel() + ?.capitalize() + } + } + val SingularName by lazy { SingularNameOrNull ?: NameUpperCamel } + + + // Field value + val mayBeNull: Boolean + get() = when { + isPrimitive -> false + "@${classPrinter.NonNull}" in annotations -> false + "@${classPrinter.NonEmpty}" in annotations -> false + isNullable -> true + lazyInitializer != null -> true + else -> classPrinter { !FeatureFlag.IMPLICIT_NONNULL() } + } + val lazyInitializer + get() = classInfo.classAst.methods.find { method -> + method.nameAsString == "lazyInit$NameUpperCamel" && method.parameters.isEmpty() + }?.nameAsString + val internalGetter get() = if (lazyInitializer != null) "get$NameUpperCamel()" else name + val defaultExpr: Any? + get() { + variableAst.initializer.orElse(null)?.let { return it } + classInfo.classAst.methods.find { + it.nameAsString == "default$NameUpperCamel" && it.parameters.isEmpty() + }?.run { return "$nameAsString()" } + return null + } + val hasDefault get() = defaultExpr != null + + + // Generic args + val isArray = Type.endsWith("[]") + val isList = FieldClass == "List" || FieldClass == "ArrayList" + val isMap = FieldClass == "Map" || FieldClass == "ArrayMap" + || FieldClass == "HashMap" || FieldClass == "LinkedHashMap" + val fieldBit = bitAtExpr(index) + var isLast = false + val isFinal = fieldAst.isFinal + val fieldTypeGenegicArgs = when (typeAst) { + is ArrayType -> listOf(fieldAst.elementType.asString()) + is ClassOrInterfaceType -> { + typeAst.typeArguments.orElse(null)?.map { it.asString() } ?: emptyList() + } + else -> emptyList() + } + val FieldInnerType = fieldTypeGenegicArgs.firstOrNull() + val FieldInnerClass = FieldInnerType?.takeWhile { it != '<' } + + + // Annotations + var intOrStringDef = null as ConstDef? + val annotations by lazy { + if (FieldClass in BUILTIN_SPECIAL_PARCELLINGS) { + classPrinter { + fileInfo.apply { + fieldAst.addAnnotation(SingleMemberAnnotationExpr( + Name(ParcelWith), + ClassExpr(parseJava(JavaParser::parseClassOrInterfaceType, + "$Parcelling.BuiltIn.For$FieldClass")))) + } + } + } + fieldAst.annotations.map { it.removeComment().toString() } + } + val annotationsNoInternal by lazy { + annotations.filterNot { ann -> + classPrinter { + internalAnnotations.any { + it in ann + } + } + } + } + + fun hasAnnotation(a: String) = annotations.any { it.startsWith(a) } + val isNullable by lazy { hasAnnotation("@Nullable") } + val isNonEmpty by lazy { hasAnnotation("@${classPrinter.NonEmpty}") } + val customParcellingClass by lazy { + fieldAst.annotations.find { it.nameAsString == classPrinter.ParcelWith } + ?.singleArgAs<ClassExpr>() + ?.type + ?.asString() + } + val annotationsAndType by lazy { (annotationsNoInternal + Type).joinToString(" ") } + val sParcelling by lazy { customParcellingClass?.let { "sParcellingFor$NameUpperCamel" } } + + val SetterParamType = if (isArray) "$FieldInnerType..." else Type + val annotationsForSetterParam by lazy { + buildList<String> { + addAll(annotationsNoInternal) + classPrinter { + if ("@$Nullable" in annotations + && "@$MaySetToNull" !in annotations) { + remove("@$Nullable") + add("@$NonNull") + } + } + }.joinToString(" ") + } + val annotatedTypeForSetterParam by lazy { "$annotationsForSetterParam $SetterParamType" } + + // Utilities + + /** + * `mFoo.size()` + */ + val ClassPrinter.sizeExpr get() = when { + isArray && FieldInnerClass !in PRIMITIVE_TYPES -> + memberRef("com.android.internal.util.ArrayUtils.size") + "($name)" + isArray -> "$name.length" + listOf("List", "Set", "Map").any { FieldClass.endsWith(it) } -> + memberRef("com.android.internal.util.CollectionUtils.size") + "($name)" + Type == "String" -> memberRef("android.text.TextUtils.length") + "($name)" + Type == "CharSequence" -> "$name.length()" + else -> "$name.size()" + } + /** + * `mFoo.get(0)` + */ + fun elemAtIndexExpr(indexExpr: String) = when { + isArray -> "$name[$indexExpr]" + FieldClass == "ArraySet" -> "$name.valueAt($indexExpr)" + else -> "$name.get($indexExpr)" + } + /** + * `mFoo.isEmpty()` + */ + val ClassPrinter.isEmptyExpr get() = when { + isArray || Type == "CharSequence" -> "$sizeExpr == 0" + else -> "$name.isEmpty()" + } + + /** + * `mFoo == that` or `Objects.equals(mFoo, that)`, etc. + */ + fun ClassPrinter.isEqualToExpr(that: String) = when { + Type in PRIMITIVE_TYPES -> "$internalGetter == $that" + isArray -> "${memberRef("java.util.Arrays.equals")}($internalGetter, $that)" + else -> "${memberRef("java.util.Objects.equals")}($internalGetter, $that)" + } + + /** + * Parcel.write* and Parcel.read* method name wildcard values + */ + val ParcelMethodsSuffix = when { + FieldClass in PRIMITIVE_TYPES - "char" - "boolean" + BOXED_PRIMITIVE_TYPES + + listOf("String", "CharSequence", "Exception", "Size", "SizeF", "Bundle", + "FileDescriptor", "SparseBooleanArray", "SparseIntArray", "SparseArray") -> + FieldClass + isMap && fieldTypeGenegicArgs[0] == "String" -> "Map" + isArray -> when { + FieldInnerType!! in (PRIMITIVE_TYPES + "String") -> FieldInnerType + "Array" + isBinder(FieldInnerType) -> "BinderArray" + else -> "TypedArray" + } + isList -> when { + FieldInnerType == "String" -> "StringList" + isBinder(FieldInnerType!!) -> "BinderList" + else -> "ParcelableList" + } + isIInterface(Type) -> "StrongInterface" + isBinder(Type) -> "StrongBinder" + else -> "TypedObject" + }.capitalize() + + private fun isBinder(type: String) = type == "Binder" || type == "IBinder" || isIInterface(type) + private fun isIInterface(type: String) = type.length >= 2 && type[0] == 'I' && type[1].isUpperCase() +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/FileInfo.kt b/tools/codegen/src/com/android/codegen/FileInfo.kt new file mode 100644 index 000000000000..909472640f29 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/FileInfo.kt @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.codegen + +import com.github.javaparser.JavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.body.TypeDeclaration +import java.io.File + +/** + * File-level parsing & printing logic + * + * @see [main] entrypoint + */ +class FileInfo( + val sourceLines: List<String>, + val cliArgs: Array<String>, + val file: File) + : Printer<FileInfo>, ImportsProvider { + + override val fileAst: CompilationUnit + = parseJava(JavaParser::parse, sourceLines.joinToString("\n")) + + override val stringBuilder = StringBuilder() + override var currentIndent = INDENT_SINGLE + + + val generatedWarning = run { + val fileEscaped = file.absolutePath.replace( + System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP") + + """ + + + // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ $THIS_SCRIPT_LOCATION$CODEGEN_NAME ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + """ + } + private val generatedWarningNumPrecedingEmptyLines + = generatedWarning.lines().takeWhile { it.isBlank() }.size + + val classes = fileAst.types + .filterIsInstance<ClassOrInterfaceDeclaration>() + .flatMap { it.plusNested() } + .filterNot { it.isInterface } + + val mainClass = classes.find { it.nameAsString == file.nameWithoutExtension }!! + + // Parse stage 1 + val classBounds: List<ClassBounds> = classes.map { ast -> + ClassBounds(ast, fileInfo = this) + }.apply { + forEachApply { + if (ast.isNestedType) { + val parent = find { + it.name == (ast.parentNode.get()!! as TypeDeclaration<*>).nameAsString + }!! + parent.nested.add(this) + nestedIn = parent + } + } + } + + // Parse Stage 2 + var codeChunks = buildList<CodeChunk> { + val mainClassBounds = classBounds.find { it.nestedIn == null }!! + add(CodeChunk.FileHeader( + mainClassBounds.fileInfo.sourceLines.subList(0, mainClassBounds.range.start))) + add(CodeChunk.DataClass.parse(mainClassBounds)) + } + + // Output stage + fun main() { + codeChunks.forEach { print(it) } + } + + fun print(chunk: CodeChunk) { + when(chunk) { + is CodeChunk.GeneratedCode -> { + // Re-parse class code, discarding generated code and nested dataclasses + val ast = chunk.owner.chunks + .filter { + it.javaClass == CodeChunk.Code::class.java + || it.javaClass == CodeChunk.ClosingBrace::class.java + } + .flatMap { (it as CodeChunk.Code).lines } + .joinToString("\n") + .let { + parseJava(JavaParser::parseTypeDeclaration, it) + as ClassOrInterfaceDeclaration + } + + // Write new generated code + ClassPrinter(ast, fileInfo = this).print() + } + is CodeChunk.ClosingBrace -> { + // Special case - print closing brace with -1 indent + rmEmptyLine() + popIndent() + +"\n}" + } + // Print general code as-is + is CodeChunk.Code -> chunk.lines.forEach { stringBuilder.appendln(it) } + // Recursively render data classes + is CodeChunk.DataClass -> chunk.chunks.forEach { print(it) } + } + } + + /** + * Output of stage 1 of parsing a file: + * Recursively nested ranges of code line numbers containing nested classes + */ + data class ClassBounds( + val ast: ClassOrInterfaceDeclaration, + val fileInfo: FileInfo, + val name: String = ast.nameAsString, + val range: ClosedRange<Int> = ast.range.get()!!.let { rng -> rng.begin.line-1..rng.end.line-1 }, + val nested: MutableList<ClassBounds> = mutableListOf(), + var nestedIn: ClassBounds? = null) { + + val nestedDataClasses: List<ClassBounds> by lazy { + nested.filter { it.isDataclass }.sortedBy { it.range.start } + } + val isDataclass = ast.annotations.any { it.nameAsString.endsWith("DataClass") } + + val baseIndentLength = fileInfo.sourceLines.find { "class $name" in it }!!.takeWhile { it == ' ' }.length + val baseIndent = buildString { repeat(baseIndentLength) { append(' ') } } + + val sourceNoPrefix = fileInfo.sourceLines.drop(range.start) + val generatedCodeRange = sourceNoPrefix + .indexOfFirst { it.startsWith("$baseIndent$INDENT_SINGLE// $GENERATED_WARNING_PREFIX") } + .let { start -> + if (start < 0) { + null + } else { + var endInclusive = sourceNoPrefix.indexOfFirst { + it.startsWith("$baseIndent$INDENT_SINGLE$GENERATED_END") + } + if (endInclusive == -1) { + // Legacy generated code doesn't have end markers + endInclusive = sourceNoPrefix.size - 2 + } + IntRange( + range.start + start - fileInfo.generatedWarningNumPrecedingEmptyLines, + range.start + endInclusive) + } + } + + /** Debug info */ + override fun toString(): String { + return buildString { + appendln("class $name $range") + nested.forEach { + appendln(it) + } + appendln("end $name") + } + } + } + + /** + * Output of stage 2 of parsing a file + */ + sealed class CodeChunk { + /** General code */ + open class Code(val lines: List<String>): CodeChunk() {} + + /** Copyright + package + imports + main javadoc */ + class FileHeader(lines: List<String>): Code(lines) + + /** Code to be discarded and refreshed */ + open class GeneratedCode(lines: List<String>): Code(lines) { + lateinit var owner: DataClass + + class Placeholder: GeneratedCode(emptyList()) + } + + object ClosingBrace: Code(listOf("}")) + + data class DataClass( + val ast: ClassOrInterfaceDeclaration, + val chunks: List<CodeChunk>, + val generatedCode: GeneratedCode?): CodeChunk() { + + companion object { + fun parse(classBounds: ClassBounds): DataClass { + val initial = Code(lines = classBounds.fileInfo.sourceLines.subList( + fromIndex = classBounds.range.start, + toIndex = findLowerBound( + thisClass = classBounds, + nextNestedClass = classBounds.nestedDataClasses.getOrNull(0)))) + + val chunks = mutableListOf<CodeChunk>(initial) + + classBounds.nestedDataClasses.forEachSequentialPair { + nestedDataClass, nextNestedDataClass -> + chunks += DataClass.parse(nestedDataClass) + chunks += Code(lines = classBounds.fileInfo.sourceLines.subList( + fromIndex = nestedDataClass.range.endInclusive + 1, + toIndex = findLowerBound( + thisClass = classBounds, + nextNestedClass = nextNestedDataClass))) + } + + var generatedCode = classBounds.generatedCodeRange?.let { rng -> + GeneratedCode(classBounds.fileInfo.sourceLines.subList( + rng.start, rng.endInclusive+1)) + } + if (generatedCode != null) { + chunks += generatedCode + chunks += ClosingBrace + } else if (classBounds.isDataclass) { + + // Insert placeholder for generated code to be inserted for the 1st time + chunks.last = (chunks.last as Code) + .lines + .dropLastWhile { it.isBlank() } + .run { + if (last().dropWhile { it.isWhitespace() }.startsWith("}")) { + dropLast(1) + } else { + this + } + }.let { Code(it) } + generatedCode = GeneratedCode.Placeholder() + chunks += generatedCode + chunks += ClosingBrace + } else { + // Outer class may be not a @DataClass but contain ones + // so just skip generated code for them + } + + return DataClass(classBounds.ast, chunks, generatedCode).also { + generatedCode?.owner = it + } + } + + private fun findLowerBound(thisClass: ClassBounds, nextNestedClass: ClassBounds?): Int { + return nextNestedClass?.range?.start + ?: thisClass.generatedCodeRange?.start + ?: thisClass.range.endInclusive + 1 + } + } + } + + /** Debug info */ + fun summary(): String = when(this) { + is Code -> "${javaClass.simpleName}(${lines.size} lines): ${lines.getOrNull(0)?.take(70) ?: ""}..." + is DataClass -> "DataClass ${ast.nameAsString}:\n" + + chunks.joinToString("\n") { it.summary() } + + "\n//end ${ast.nameAsString}" + } + } + + private fun ClassOrInterfaceDeclaration.plusNested(): List<ClassOrInterfaceDeclaration> { + return mutableListOf<ClassOrInterfaceDeclaration>().apply { + add(this@plusNested) + childNodes.filterIsInstance<ClassOrInterfaceDeclaration>() + .flatMap { it.plusNested() } + .let { addAll(it) } + } + } +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/Generators.kt b/tools/codegen/src/com/android/codegen/Generators.kt new file mode 100644 index 000000000000..5a96cf1d9bdb --- /dev/null +++ b/tools/codegen/src/com/android/codegen/Generators.kt @@ -0,0 +1,949 @@ +package com.android.codegen + +import com.github.javaparser.ast.body.FieldDeclaration +import com.github.javaparser.ast.body.MethodDeclaration +import com.github.javaparser.ast.body.VariableDeclarator +import com.github.javaparser.ast.expr.* +import java.io.File + + +/** + * IntDefs and StringDefs based on constants + */ +fun ClassPrinter.generateConstDefs() { + val consts = classAst.fields.filter { + it.isStatic && it.isFinal && it.variables.all { variable -> + val initializer = variable.initializer.orElse(null) + val isLiteral = initializer is LiteralExpr + || (initializer is UnaryExpr && initializer.expression is LiteralExpr) + isLiteral && variable.type.asString() in listOf("int", "String") + } && it.annotations.none { it.nameAsString == DataClassSuppressConstDefs } + }.flatMap { field -> field.variables.map { it to field } } + val intConsts = consts.filter { it.first.type.asString() == "int" } + val strConsts = consts.filter { it.first.type.asString() == "String" } + val intGroups = intConsts.groupBy { it.first.nameAsString.split("_")[0] }.values + val strGroups = strConsts.groupBy { it.first.nameAsString.split("_")[0] }.values + intGroups.forEach { + generateConstDef(it) + } + strGroups.forEach { + generateConstDef(it) + } +} + +fun ClassPrinter.generateConstDef(consts: List<Pair<VariableDeclarator, FieldDeclaration>>) { + if (consts.size <= 1) return + + val names = consts.map { it.first.nameAsString!! } + val prefix = names + .reduce { a, b -> a.commonPrefixWith(b) } + .dropLastWhile { it != '_' } + .dropLast(1) + if (prefix.isEmpty()) { + println("Failed to generate const def for $names") + return + } + var AnnotationName = prefix.split("_") + .filterNot { it.isBlank() } + .map { it.toLowerCase().capitalize() } + .joinToString("") + val annotatedConst = consts.find { it.second.annotations.isNonEmpty } + if (annotatedConst != null) { + AnnotationName = annotatedConst.second.annotations.first().nameAsString + } + val type = consts[0].first.type.asString() + val flag = type == "int" && consts.all { it.first.initializer.get().toString().startsWith("0x") } + val constDef = ConstDef(type = when { + type == "String" -> ConstDef.Type.STRING + flag -> ConstDef.Type.INT_FLAGS + else -> ConstDef.Type.INT + }, + AnnotationName = AnnotationName, + values = consts.map { it.second } + ) + constDefs += constDef + fields.forEachApply { + if (fieldAst.annotations.any { it.nameAsString == AnnotationName }) { + this.intOrStringDef = constDef + } + } + + val visibility = if (consts[0].second.isPublic) "public" else "/* package-private */" + + val Retention = classRef("java.lang.annotation.Retention") + val RetentionPolicySource = memberRef("java.lang.annotation.RetentionPolicy.SOURCE") + val ConstDef = classRef("android.annotation.${type.capitalize()}Def") + + if (FeatureFlag.CONST_DEFS.hidden) { + +"/** @hide */" + } + "@$ConstDef(${if_(flag, "flag = true, ")}prefix = \"${prefix}_\", value = {" { + names.forEachLastAware { name, isLast -> + +"$name${if_(!isLast, ",")}" + } + } + ")" + +"@$Retention($RetentionPolicySource)" + +GENERATED_MEMBER_HEADER + +"$visibility @interface $AnnotationName {}" + +"" + + if (type == "int") { + if (FeatureFlag.CONST_DEFS.hidden) { + +"/** @hide */" + } + +GENERATED_MEMBER_HEADER + val methodDefLine = "$visibility static String ${AnnotationName.decapitalize()}ToString(" + + "@$AnnotationName int value)" + if (flag) { + val flg2str = memberRef("com.android.internal.util.BitUtils.flagsToString") + methodDefLine { + "return $flg2str(" { + +"value, $ClassName::single${AnnotationName}ToString" + } + ";" + } + +GENERATED_MEMBER_HEADER + !"static String single${AnnotationName}ToString(@$AnnotationName int value)" + } else { + !methodDefLine + } + " {" { + "switch (value) {" { + names.forEach { name -> + "case $name:" { + +"return \"$name\";" + } + } + +"default: return Integer.toHexString(value);" + } + } + } +} + +fun FileInfo.generateAidl() { + val aidl = File(file.path.substringBeforeLast(".java") + ".aidl") + if (aidl.exists()) return + aidl.writeText(buildString { + sourceLines.dropLastWhile { !it.startsWith("package ") }.forEach { + appendln(it) + } + append("\nparcelable ${mainClass.nameAsString};\n") + }) +} + +/** + * ``` + * Foo newFoo = oldFoo.withBar(newBar); + * ``` + */ +fun ClassPrinter.generateWithers() { + fields.forEachApply { + val metodName = "with$NameUpperCamel" + if (!isMethodGenerationSuppressed(metodName, Type)) { + generateFieldJavadoc(forceHide = FeatureFlag.WITHERS.hidden) + """@$NonNull + $GENERATED_MEMBER_HEADER + public $ClassType $metodName($annotatedTypeForSetterParam value)""" { + val changedFieldName = name + + "return new $ClassType(" { + fields.forEachTrimmingTrailingComma { + if (name == changedFieldName) +"value," else +"$name," + } + } + ";" + } + } + } +} + +fun ClassPrinter.generateCopyConstructor() { + if (classAst.constructors.any { + it.parameters.size == 1 && + it.parameters[0].type.asString() == ClassType + }) { + return + } + + +"/** Copy constructor */" + +GENERATED_MEMBER_HEADER + "public $ClassName(@$NonNull $ClassName orig)" { + fields.forEachApply { + +"$name = orig.$name;" + } + } +} + +/** + * ``` + * Foo newFoo = oldFoo.buildUpon().setBar(newBar).build(); + * ``` + */ +fun ClassPrinter.generateBuildUpon() { + if (isMethodGenerationSuppressed("buildUpon")) return + + +"/**" + +" * Provides an instance of {@link $BuilderClass} with state corresponding to this instance." + if (FeatureFlag.BUILD_UPON.hidden) { + +" * @hide" + } + +" */" + +GENERATED_MEMBER_HEADER + "public $BuilderType buildUpon()" { + "return new $BuilderType()" { + fields.forEachApply { + +".set$NameUpperCamel($internalGetter)" + } + ";" + } + } +} + +fun ClassPrinter.generateBuilder() { + val setterVisibility = if (cliArgs.contains(FLAG_BUILDER_PROTECTED_SETTERS)) + "protected" else "public" + val constructorVisibility = if (BuilderClass == CANONICAL_BUILDER_CLASS) + "public" else "/* package-*/" + + val providedSubclassAst = nestedClasses.find { + it.extendedTypes.any { it.nameAsString == BASE_BUILDER_CLASS } + } + + val BuilderSupertype = if (customBaseBuilderAst != null) { + customBaseBuilderAst!!.nameAsString + } else { + "Object" + } + + val maybeFinal = if_(classAst.isFinal, "final ") + + +"/**" + +" * A builder for {@link $ClassName}" + if (FeatureFlag.BUILDER.hidden) +" * @hide" + +" */" + +"@SuppressWarnings(\"WeakerAccess\")" + +GENERATED_MEMBER_HEADER + !"public static ${maybeFinal}class $BuilderClass$genericArgs" + if (BuilderSupertype != "Object") { + appendSameLine(" extends $BuilderSupertype") + } + " {" { + + +"" + fields.forEachApply { + +"private $annotationsAndType $name;" + } + +"" + +"private long mBuilderFieldsSet = 0L;" + +"" + + val requiredFields = fields.filter { !it.hasDefault } + + generateConstructorJavadoc( + fields = requiredFields, + ClassName = BuilderClass, + hidden = false) + "$constructorVisibility $BuilderClass(" { + requiredFields.forEachLastAware { field, isLast -> + +"${field.annotationsAndType} ${field._name}${if_(!isLast, ",")}" + } + }; " {" { + requiredFields.forEachApply { + generateSetFrom(_name) + } + } + + generateBuilderSetters(setterVisibility) + + generateBuilderBuild() + + "private void checkNotUsed() {" { + "if ((mBuilderFieldsSet & ${bitAtExpr(fields.size)}) != 0)" { + "throw new IllegalStateException(" { + +"\"This Builder should not be reused. Use a new Builder instance instead\"" + } + +";" + } + } + + rmEmptyLine() + } +} + +private fun ClassPrinter.generateBuilderMethod( + defVisibility: String, + name: String, + paramAnnotations: String? = null, + paramTypes: List<String>, + paramNames: List<String> = listOf("value"), + genJavadoc: ClassPrinter.() -> Unit, + genBody: ClassPrinter.() -> Unit) { + + val providedMethod = customBaseBuilderAst?.members?.find { + it is MethodDeclaration + && it.nameAsString == name + && it.parameters.map { it.typeAsString } == paramTypes.toTypedArray().toList() + } as? MethodDeclaration + + if ((providedMethod == null || providedMethod.isAbstract) + && name !in builderSuppressedMembers) { + val visibility = providedMethod?.visibility?.asString() ?: defVisibility + val ReturnType = providedMethod?.typeAsString ?: CANONICAL_BUILDER_CLASS + val Annotations = providedMethod?.annotations?.joinToString("\n") + + genJavadoc() + +GENERATED_MEMBER_HEADER + if (providedMethod?.isAbstract == true) +"@Override" + if (!Annotations.isNullOrEmpty()) +Annotations + val ParamAnnotations = if (!paramAnnotations.isNullOrEmpty()) "$paramAnnotations " else "" + + "$visibility @$NonNull $ReturnType $name(${ + paramTypes.zip(paramNames).joinToString(", ") { (Type, paramName) -> + "$ParamAnnotations$Type $paramName" + } + })" { + genBody() + } + } +} + +private fun ClassPrinter.generateBuilderSetters(visibility: String) { + + fields.forEachApply { + val maybeCast = + if_(BuilderClass != CANONICAL_BUILDER_CLASS, " ($CANONICAL_BUILDER_CLASS)") + + val setterName = "set$NameUpperCamel" + + generateBuilderMethod( + name = setterName, + defVisibility = visibility, + paramAnnotations = annotationsForSetterParam, + paramTypes = listOf(SetterParamType), + genJavadoc = { generateFieldJavadoc() }) { + +"checkNotUsed();" + +"mBuilderFieldsSet |= $fieldBit;" + +"$name = value;" + +"return$maybeCast this;" + } + + val javadocSeeSetter = "/** @see #$setterName */" + val adderName = "add$SingularName" + + val singularNameCustomizationHint = if (SingularNameOrNull == null) { + "// You can refine this method's name by providing item's singular name, e.g.:\n" + + "// @DataClass.PluralOf(\"item\")) mItems = ...\n\n" + } else "" + + + if (isList && FieldInnerType != null) { + generateBuilderMethod( + name = adderName, + defVisibility = visibility, + paramAnnotations = "@$NonNull", + paramTypes = listOf(FieldInnerType), + genJavadoc = { +javadocSeeSetter }) { + + !singularNameCustomizationHint + +"if ($name == null) $setterName(new $ArrayList<>());" + +"$name.add(value);" + +"return$maybeCast this;" + } + } + + if (isMap && FieldInnerType != null) { + generateBuilderMethod( + name = adderName, + defVisibility = visibility, + paramAnnotations = "@$NonNull", + paramTypes = fieldTypeGenegicArgs, + paramNames = listOf("key", "value"), + genJavadoc = { +javadocSeeSetter }) { + !singularNameCustomizationHint + +"if ($name == null) $setterName(new ${if (FieldClass == "Map") LinkedHashMap else FieldClass}());" + +"$name.put(key, value);" + +"return$maybeCast this;" + } + } + } +} + +private fun ClassPrinter.generateBuilderBuild() { + +"/** Builds the instance. This builder should not be touched after calling this! */" + "public @$NonNull $ClassType build()" { + +"checkNotUsed();" + +"mBuilderFieldsSet |= ${bitAtExpr(fields.size)}; // Mark builder used" + +"" + fields.forEachApply { + if (hasDefault) { + "if ((mBuilderFieldsSet & $fieldBit) == 0)" { + +"$name = $defaultExpr;" + } + } + } + "$ClassType o = new $ClassType(" { + fields.forEachTrimmingTrailingComma { + +"$name," + } + } + ";" + +"return o;" + } +} + +fun ClassPrinter.generateParcelable() { + val booleanFields = fields.filter { it.Type == "boolean" } + val objectFields = fields.filter { it.Type !in PRIMITIVE_TYPES } + val nullableFields = objectFields.filter { it.mayBeNull } + val nonBooleanFields = fields - booleanFields + + + val flagStorageType = when (fields.size) { + in 0..7 -> "byte" + in 8..15 -> "int" + in 16..31 -> "long" + else -> throw NotImplementedError("32+ field classes not yet supported") + } + val FlagStorageType = flagStorageType.capitalize() + + fields.forEachApply { + if (sParcelling != null) { + +GENERATED_MEMBER_HEADER + "static $Parcelling<$Type> $sParcelling =" { + "$Parcelling.Cache.get(" { + +"$customParcellingClass.class" + } + ";" + } + "static {" { + "if ($sParcelling == null)" { + "$sParcelling = $Parcelling.Cache.put(" { + +"new $customParcellingClass()" + } + ";" + } + } + +"" + } + } + + val Parcel = classRef("android.os.Parcel") + if (!isMethodGenerationSuppressed("writeToParcel", Parcel, "int")) { + +"@Override" + +GENERATED_MEMBER_HEADER + "public void writeToParcel(@$NonNull $Parcel dest, int flags)" { + +"// You can override field parcelling by defining methods like:" + +"// void parcelFieldName(Parcel dest, int flags) { ... }" + +"" + + if (extendsParcelableClass) { + +"super.writeToParcel(dest, flags);\n" + } + + if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) { + +"$flagStorageType flg = 0;" + booleanFields.forEachApply { + +"if ($internalGetter) flg |= $fieldBit;" + } + nullableFields.forEachApply { + +"if ($internalGetter != null) flg |= $fieldBit;" + } + +"dest.write$FlagStorageType(flg);" + } + + nonBooleanFields.forEachApply { + val customParcellingMethod = "parcel$NameUpperCamel" + when { + hasMethod(customParcellingMethod, Parcel, "int") -> + +"$customParcellingMethod(dest, flags);" + customParcellingClass != null -> +"$sParcelling.parcel($name, dest, flags);" + hasAnnotation("@$DataClassEnum") -> + +"dest.writeInt($internalGetter == null ? -1 : $internalGetter.ordinal());" + else -> { + if (mayBeNull) !"if ($internalGetter != null) " + var args = internalGetter + if (ParcelMethodsSuffix.startsWith("Parcelable") + || ParcelMethodsSuffix.startsWith("TypedObject") + || ParcelMethodsSuffix == "TypedArray") { + args += ", flags" + } + +"dest.write$ParcelMethodsSuffix($args);" + } + } + } + } + } + + if (!isMethodGenerationSuppressed("describeContents")) { + +"@Override" + +GENERATED_MEMBER_HEADER + +"public int describeContents() { return 0; }" + +"" + } + + if (!hasMethod(ClassName, Parcel)) { + val visibility = if (classAst.isFinal) "/* package-private */" else "protected" + + +"/** @hide */" + +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})" + +GENERATED_MEMBER_HEADER + "$visibility $ClassName(@$NonNull $Parcel in) {" { + +"// You can override field unparcelling by defining methods like:" + +"// static FieldType unparcelFieldName(Parcel in) { ... }" + +"" + + if (extendsParcelableClass) { + +"super(in);\n" + } + + if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) { + +"$flagStorageType flg = in.read$FlagStorageType();" + } + booleanFields.forEachApply { + +"$Type $_name = (flg & $fieldBit) != 0;" + } + nonBooleanFields.forEachApply { + + // Handle customized parceling + val customParcellingMethod = "unparcel$NameUpperCamel" + if (hasMethod(customParcellingMethod, Parcel)) { + +"$Type $_name = $customParcellingMethod(in);" + } else if (customParcellingClass != null) { + +"$Type $_name = $sParcelling.unparcel(in);" + } else if (hasAnnotation("@$DataClassEnum")) { + val ordinal = "${_name}Ordinal" + +"int $ordinal = in.readInt();" + +"$Type $_name = $ordinal < 0 ? null : $FieldClass.values()[$ordinal];" + } else { + val methodArgs = mutableListOf<String>() + + // Create container if any + val containerInitExpr = when { + FieldClass == "Map" -> "new $LinkedHashMap<>()" + isMap -> "new $FieldClass()" + FieldClass == "List" || FieldClass == "ArrayList" -> + "new ${classRef("java.util.ArrayList")}<>()" + else -> "" + } + val passContainer = containerInitExpr.isNotEmpty() + + // nullcheck + + // "FieldType fieldName = (FieldType)" + if (passContainer) { + methodArgs.add(_name) + !"$Type $_name = " + if (mayBeNull) { + +"null;" + !"if ((flg & $fieldBit) != 0) {" + pushIndent() + +"" + !"$_name = " + } + +"$containerInitExpr;" + } else { + !"$Type $_name = " + if (mayBeNull) !"(flg & $fieldBit) == 0 ? null : " + if (ParcelMethodsSuffix == "StrongInterface") { + !"$FieldClass.Stub.asInterface(" + } else if (Type !in PRIMITIVE_TYPES + "String" + "Bundle" && + (!isArray || FieldInnerType !in PRIMITIVE_TYPES + "String") && + ParcelMethodsSuffix != "Parcelable") { + !"($FieldClass) " + } + } + + // Determine method args + when { + ParcelMethodsSuffix == "Parcelable" -> + methodArgs += "$FieldClass.class.getClassLoader()" + ParcelMethodsSuffix == "SparseArray" -> + methodArgs += "$FieldInnerClass.class.getClassLoader()" + ParcelMethodsSuffix == "TypedObject" -> + methodArgs += "$FieldClass.CREATOR" + ParcelMethodsSuffix == "TypedArray" -> + methodArgs += "$FieldInnerClass.CREATOR" + ParcelMethodsSuffix == "Map" -> + methodArgs += "${fieldTypeGenegicArgs[1].substringBefore("<")}.class.getClassLoader()" + ParcelMethodsSuffix.startsWith("Parcelable") + || (isList || isArray) + && FieldInnerType !in PRIMITIVE_TYPES + "String" -> + methodArgs += "$FieldInnerClass.class.getClassLoader()" + } + + // ...in.readFieldType(args...); + when { + ParcelMethodsSuffix == "StrongInterface" -> !"in.readStrongBinder" + isArray -> !"in.create$ParcelMethodsSuffix" + else -> !"in.read$ParcelMethodsSuffix" + } + !"(${methodArgs.joinToString(", ")})" + if (ParcelMethodsSuffix == "StrongInterface") !")" + +";" + + // Cleanup if passContainer + if (passContainer && mayBeNull) { + popIndent() + rmEmptyLine() + +"\n}" + } + } + } + + +"" + fields.forEachApply { + !"this." + generateSetFrom(_name) + } + + generateOnConstructedCallback() + } + } + + if (classAst.fields.none { it.variables[0].nameAsString == "CREATOR" }) { + val Creator = classRef("android.os.Parcelable.Creator") + + +GENERATED_MEMBER_HEADER + "public static final @$NonNull $Creator<$ClassName> CREATOR" { + +"= new $Creator<$ClassName>()" + }; " {" { + + +"@Override" + "public $ClassName[] newArray(int size)" { + +"return new $ClassName[size];" + } + + +"@Override" + "public $ClassName createFromParcel(@$NonNull $Parcel in)" { + +"return new $ClassName(in);" + } + rmEmptyLine() + } + ";" + +"" + } +} + +fun ClassPrinter.generateEqualsHashcode() { + if (!isMethodGenerationSuppressed("equals", "Object")) { + +"@Override" + +GENERATED_MEMBER_HEADER + "public boolean equals(@$Nullable Object o)" { + +"// You can override field equality logic by defining either of the methods like:" + +"// boolean fieldNameEquals($ClassName other) { ... }" + +"// boolean fieldNameEquals(FieldType otherValue) { ... }" + +"" + """if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + $ClassType that = ($ClassType) o; + //noinspection PointlessBooleanExpression + return true""" { + fields.forEachApply { + val sfx = if (isLast) ";" else "" + val customEquals = "${nameLowerCamel}Equals" + when { + hasMethod(customEquals, Type) -> +"&& $customEquals(that.$internalGetter)$sfx" + hasMethod(customEquals, ClassType) -> +"&& $customEquals(that)$sfx" + else -> +"&& ${isEqualToExpr("that.$internalGetter")}$sfx" + } + } + } + } + } + + if (!isMethodGenerationSuppressed("hashCode")) { + +"@Override" + +GENERATED_MEMBER_HEADER + "public int hashCode()" { + +"// You can override field hashCode logic by defining methods like:" + +"// int fieldNameHashCode() { ... }" + +"" + +"int _hash = 1;" + fields.forEachApply { + !"_hash = 31 * _hash + " + val customHashCode = "${nameLowerCamel}HashCode" + when { + hasMethod(customHashCode) -> +"$customHashCode();" + Type == "int" || Type == "byte" -> +"$internalGetter;" + Type in PRIMITIVE_TYPES -> +"${Type.capitalize()}.hashCode($internalGetter);" + isArray -> +"${memberRef("java.util.Arrays.hashCode")}($internalGetter);" + else -> +"${memberRef("java.util.Objects.hashCode")}($internalGetter);" + } + } + +"return _hash;" + } + } +} + +//TODO support IntDef flags? +fun ClassPrinter.generateToString() { + if (!isMethodGenerationSuppressed("toString")) { + +"@Override" + +GENERATED_MEMBER_HEADER + "public String toString()" { + +"// You can override field toString logic by defining methods like:" + +"// String fieldNameToString() { ... }" + +"" + "return \"$ClassName { \" +" { + fields.forEachApply { + val customToString = "${nameLowerCamel}ToString" + val expr = when { + hasMethod(customToString) -> "$customToString()" + isArray -> "${memberRef("java.util.Arrays.toString")}($internalGetter)" + intOrStringDef?.type?.isInt == true -> + "${intOrStringDef!!.AnnotationName.decapitalize()}ToString($name)" + else -> internalGetter + } + +"\"$nameLowerCamel = \" + $expr${if_(!isLast, " + \", \"")} +" + } + } + +"\" }\";" + } + } +} + +fun ClassPrinter.generateSetters() { + fields.forEachApply { + if (!isMethodGenerationSuppressed("set$NameUpperCamel", Type) + && !fieldAst.isPublic + && !isFinal) { + + generateFieldJavadoc(forceHide = FeatureFlag.SETTERS.hidden) + +GENERATED_MEMBER_HEADER + "public $ClassType set$NameUpperCamel($annotatedTypeForSetterParam value)" { + generateSetFrom("value") + +"return this;" + } + } + } +} + +fun ClassPrinter.generateGetters() { + (fields + lazyTransientFields).forEachApply { + val methodPrefix = if (Type == "boolean") "is" else "get" + val methodName = methodPrefix + NameUpperCamel + + if (!isMethodGenerationSuppressed(methodName) && !fieldAst.isPublic) { + + generateFieldJavadoc(forceHide = FeatureFlag.GETTERS.hidden) + +GENERATED_MEMBER_HEADER + "public $annotationsAndType $methodName()" { + if (lazyInitializer == null) { + +"return $name;" + } else { + +"$Type $_name = $name;" + "if ($_name == null)" { + if (fieldAst.isVolatile) { + "synchronized(this)" { + +"$_name = $name;" + "if ($_name == null)" { + +"$_name = $name = $lazyInitializer();" + } + } + } else { + +"// You can mark field as volatile for thread-safe double-check init" + +"$_name = $name = $lazyInitializer();" + } + } + +"return $_name;" + } + } + } + } +} + +fun FieldInfo.generateFieldJavadoc(forceHide: Boolean = false) = classPrinter { + if (javadocFull != null || forceHide) { + var hidden = false + (javadocFull ?: "/**\n */").lines().forEach { + if (it.contains("@hide")) hidden = true + if (it.contains("*/") && forceHide && !hidden) { + if (javadocFull != null) +" *" + +" * @hide" + } + +it + } + } +} + +fun FieldInfo.generateSetFrom(source: String) = classPrinter { + +"$name = $source;" + generateFieldValidation(field = this@generateSetFrom) +} + +fun ClassPrinter.generateConstructor(visibility: String = "public") { + if (visibility == "public") { + generateConstructorJavadoc() + } + +GENERATED_MEMBER_HEADER + "$visibility $ClassName(" { + fields.forEachApply { + +"$annotationsAndType $nameLowerCamel${if_(!isLast, ",")}" + } + } + " {" { + fields.forEachApply { + !"this." + generateSetFrom(nameLowerCamel) + } + + generateOnConstructedCallback() + } +} + +private fun ClassPrinter.generateConstructorJavadoc( + fields: List<FieldInfo> = this.fields, + ClassName: String = this.ClassName, + hidden: Boolean = FeatureFlag.CONSTRUCTOR.hidden) { + if (fields.all { it.javadoc == null } && !FeatureFlag.CONSTRUCTOR.hidden) return + +"/**" + +" * Creates a new $ClassName." + +" *" + fields.filter { it.javadoc != null }.forEachApply { + javadocTextNoAnnotationLines?.apply { + +" * @param $nameLowerCamel" + forEach { + +" * $it" + } + } + } + if (FeatureFlag.CONSTRUCTOR.hidden) +" * @hide" + +" */" +} + +private fun ClassPrinter.appendLinesWithContinuationIndent(text: String) { + val lines = text.lines() + if (lines.isNotEmpty()) { + !lines[0] + } + if (lines.size >= 2) { + "" { + lines.drop(1).forEach { + +it + } + } + } +} + +private fun ClassPrinter.generateFieldValidation(field: FieldInfo) = field.run { + if (isNonEmpty) { + "if ($isEmptyExpr)" { + +"throw new IllegalArgumentException(\"$nameLowerCamel cannot be empty\");" + } + } + if (intOrStringDef != null) { + if (intOrStringDef!!.type == ConstDef.Type.INT_FLAGS) { + +"" + "$Preconditions.checkFlagsArgument(" { + +"$name, " + appendLinesWithContinuationIndent(intOrStringDef!!.CONST_NAMES.joinToString("\n| ")) + } + +";" + } else { + +"" + !"if (" + appendLinesWithContinuationIndent(intOrStringDef!!.CONST_NAMES.joinToString("\n&& ") { + "!(${isEqualToExpr(it)})" + }) + rmEmptyLine(); ") {" { + "throw new ${classRef<IllegalArgumentException>()}(" { + "\"$nameLowerCamel was \" + $internalGetter + \" but must be one of: \"" { + + intOrStringDef!!.CONST_NAMES.forEachLastAware { CONST_NAME, isLast -> + +"""+ "$CONST_NAME(" + $CONST_NAME + ")${if_(!isLast, ", ")}"""" + } + } + } + +";" + } + } + } + + val eachLine = fieldAst.annotations.find { it.nameAsString == Each }?.range?.orElse(null)?.end?.line + val perElementValidations = if (eachLine == null) emptyList() else fieldAst.annotations.filter { + it.nameAsString != Each && + it.range.orElse(null)?.begin?.line?.let { it >= eachLine } ?: false + } + + val Size = classRef("android.annotation.Size") + fieldAst.annotations.filterNot { + it.nameAsString == intOrStringDef?.AnnotationName + || it.nameAsString in knownNonValidationAnnotations + || it in perElementValidations + || it.args.any { (_, value) -> value is ArrayInitializerExpr } + }.forEach { annotation -> + appendValidateCall(annotation, + valueToValidate = if (annotation.nameAsString == Size) sizeExpr else name) + } + + if (perElementValidations.isNotEmpty()) { + +"int ${nameLowerCamel}Size = $sizeExpr;" + "for (int i = 0; i < ${nameLowerCamel}Size; i++) {" { + perElementValidations.forEach { annotation -> + appendValidateCall(annotation, + valueToValidate = elemAtIndexExpr("i")) + } + } + } +} + +fun ClassPrinter.appendValidateCall(annotation: AnnotationExpr, valueToValidate: String) { + val validate = memberRef("com.android.internal.util.AnnotationValidations.validate") + "$validate(" { + !"${annotation.nameAsString}.class, null, $valueToValidate" + annotation.args.forEach { name, value -> + !",\n\"$name\", $value" + } + } + +";" +} + +private fun ClassPrinter.generateOnConstructedCallback(prefix: String = "") { + +"" + val call = "${prefix}onConstructed();" + if (hasMethod("onConstructed")) { + +call + } else { + +"// $call // You can define this method to get a callback" + } +} + +fun ClassPrinter.generateForEachField() { + val specializations = listOf("Object", "int") + val usedSpecializations = fields.map { if (it.Type in specializations) it.Type else "Object" } + val usedSpecializationsSet = usedSpecializations.toSet() + + val PerObjectFieldAction = classRef("com.android.internal.util.DataClass.PerObjectFieldAction") + + +GENERATED_MEMBER_HEADER + "void forEachField(" { + usedSpecializationsSet.toList().forEachLastAware { specType, isLast -> + val SpecType = specType.capitalize() + val ActionClass = classRef("com.android.internal.util.DataClass.Per${SpecType}FieldAction") + +"@$NonNull $ActionClass<$ClassType> action$SpecType${if_(!isLast, ",")}" + } + }; " {" { + usedSpecializations.forEachIndexed { i, specType -> + val SpecType = specType.capitalize() + fields[i].apply { + +"action$SpecType.accept$SpecType(this, \"$nameLowerCamel\", $name);" + } + } + } + + if (usedSpecializationsSet.size > 1) { + +"/** @deprecated May cause boxing allocations - use with caution! */" + +"@Deprecated" + +GENERATED_MEMBER_HEADER + "void forEachField(@$NonNull $PerObjectFieldAction<$ClassType> action)" { + fields.forEachApply { + +"action.acceptObject(this, \"$nameLowerCamel\", $name);" + } + } + } +} + +fun ClassPrinter.generateMetadata(file: File) { + "@$DataClassGenerated(" { + +"time = ${System.currentTimeMillis()}L," + +"codegenVersion = \"$CODEGEN_VERSION\"," + +"sourceFile = \"${file.relativeTo(File(System.getenv("ANDROID_BUILD_TOP")))}\"," + +"inputSignatures = \"${getInputSignatures().joinToString("\\n")}\"" + } + +"" + +"@Deprecated" + +"private void __metadata() {}\n" +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/ImportsProvider.kt b/tools/codegen/src/com/android/codegen/ImportsProvider.kt new file mode 100644 index 000000000000..27dd9587db25 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/ImportsProvider.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.codegen + +import com.github.javaparser.ast.CompilationUnit + +/** + * Mixin for optionally shortening references based on existing imports + */ +interface ImportsProvider { + + abstract val fileAst: CompilationUnit + + val NonNull: String get() { return classRef("android.annotation.NonNull") } + val NonEmpty: String get() { return classRef("android.annotation.NonEmpty") } + val Nullable: String get() { return classRef("android.annotation.Nullable") } + val TextUtils: String get() { return classRef("android.text.TextUtils") } + val LinkedHashMap: String get() { return classRef("java.util.LinkedHashMap") } + val Collections: String get() { return classRef("java.util.Collections") } + val Preconditions: String get() { return classRef("com.android.internal.util.Preconditions") } + val ArrayList: String get() { return classRef("java.util.ArrayList") } + val DataClass: String get() { return classRef("com.android.internal.util.DataClass") } + val DataClassEnum: String get() { return classRef("com.android.internal.util.DataClass.Enum") } + val ParcelWith: String get() { return classRef("com.android.internal.util.DataClass.ParcelWith") } + val PluralOf: String get() { return classRef("com.android.internal.util.DataClass.PluralOf") } + val Each: String get() { return classRef("com.android.internal.util.DataClass.Each") } + val MaySetToNull: String get() { return classRef("com.android.internal.util.DataClass.MaySetToNull") } + val DataClassGenerated: String get() { return classRef("com.android.internal.util.DataClass.Generated") } + val DataClassSuppressConstDefs: String get() { return classRef("com.android.internal.util.DataClass.SuppressConstDefsGeneration") } + val DataClassSuppress: String get() { return classRef("com.android.internal.util.DataClass.Suppress") } + val GeneratedMember: String get() { return classRef("com.android.internal.util.DataClass.Generated.Member") } + val Parcelling: String get() { return classRef("com.android.internal.util.Parcelling") } + val Parcelable: String get() { return classRef("android.os.Parcelable") } + val Parcel: String get() { return classRef("android.os.Parcel") } + val UnsupportedAppUsage: String get() { return classRef("android.compat.annotation.UnsupportedAppUsage") } + + /** + * Optionally shortens a class reference if there's a corresponding import present + */ + fun classRef(fullName: String): String { + + val pkg = fullName.substringBeforeLast(".") + val simpleName = fullName.substringAfterLast(".") + if (fileAst.imports.any { imprt -> + imprt.nameAsString == fullName + || (imprt.isAsterisk && imprt.nameAsString == pkg) + }) { + return simpleName + } else { + val outerClass = pkg.substringAfterLast(".", "") + if (outerClass.firstOrNull()?.isUpperCase() == true) { + return classRef(pkg) + "." + simpleName + } + } + return fullName + } + + /** @see classRef */ + fun memberRef(fullName: String): String { + val className = fullName.substringBeforeLast(".") + val methodName = fullName.substringAfterLast(".") + return if (fileAst.imports.any { + it.isStatic + && (it.nameAsString == fullName + || (it.isAsterisk && it.nameAsString == className)) + }) { + className.substringAfterLast(".") + "." + methodName + } else { + classRef(className) + "." + methodName + } + } +} + +/** @see classRef */ +inline fun <reified T : Any> ImportsProvider.classRef(): String { + return classRef(T::class.java.name) +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt new file mode 100644 index 000000000000..d6953c00fc0b --- /dev/null +++ b/tools/codegen/src/com/android/codegen/InputSignaturesComputation.kt @@ -0,0 +1,151 @@ +package com.android.codegen + +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.expr.* +import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations +import com.github.javaparser.ast.type.ClassOrInterfaceType +import com.github.javaparser.ast.type.Type + + +fun ClassPrinter.getInputSignatures(): List<String> { + return generateInputSignaturesForClass(classAst) + + annotationToString(classAst.annotations.find { it.nameAsString == DataClass }) + + generateInputSignaturesForClass(customBaseBuilderAst) +} + +private fun ClassPrinter.generateInputSignaturesForClass(classAst: ClassOrInterfaceDeclaration?): List<String> { + if (classAst == null) return emptyList() + + return classAst.fields.map { fieldAst -> + buildString { + append(fieldAst.modifiers.joinToString(" ") { it.keyword.asString() }) + append(" ") + append(annotationsToString(fieldAst)) + append(" ") + append(getFullClassName(fieldAst.commonType)) + append(" ") + append(fieldAst.variables.joinToString(", ") { it.nameAsString }) + } + } + classAst.methods.map { methodAst -> + buildString { + append(methodAst.modifiers.joinToString(" ") { it.keyword.asString() }) + append(" ") + append(annotationsToString(methodAst)) + append(" ") + append(getFullClassName(methodAst.type)) + append(" ") + append(methodAst.nameAsString) + append("(") + append(methodAst.parameters.joinToString(",") { getFullClassName(it.type) }) + append(")") + } + } + ("class ${classAst.nameAsString}" + + " extends ${classAst.extendedTypes.map { getFullClassName(it) }.ifEmpty { listOf("java.lang.Object") }.joinToString(", ")}" + + " implements [${classAst.implementedTypes.joinToString(", ") { getFullClassName(it) }}]") +} + +private fun ClassPrinter.annotationsToString(annotatedAst: NodeWithAnnotations<*>): String { + return annotatedAst + .annotations + .groupBy { it.nameAsString } // dedupe annotations by name (javaparser bug?) + .values + .joinToString(" ") { + annotationToString(it[0]) + } +} + +private fun ClassPrinter.annotationToString(ann: AnnotationExpr?): String { + if (ann == null) return "" + return buildString { + append("@") + append(getFullClassName(ann.nameAsString)) + if (ann is MarkerAnnotationExpr) return@buildString + + append("(") + + when (ann) { + is SingleMemberAnnotationExpr -> { + appendExpr(this, ann.memberValue) + } + is NormalAnnotationExpr -> { + ann.pairs.forEachLastAware { pair, isLast -> + append(pair.nameAsString) + append("=") + appendExpr(this, pair.value) + if (!isLast) append(", ") + } + } + } + + append(")") + }.replace("\"", "\\\"") +} + +private fun ClassPrinter.appendExpr(sb: StringBuilder, ex: Expression?) { + when (ex) { + is ClassExpr -> sb.append(getFullClassName(ex.typeAsString)).append(".class") + is IntegerLiteralExpr -> sb.append(ex.asInt()).append("L") + is LongLiteralExpr -> sb.append(ex.asLong()).append("L") + is DoubleLiteralExpr -> sb.append(ex.asDouble()) + is ArrayInitializerExpr -> { + sb.append("{") + ex.values.forEachLastAware { arrayElem, isLast -> + appendExpr(sb, arrayElem) + if (!isLast) sb.append(", ") + } + sb.append("}") + } + else -> sb.append(ex) + } +} + +private fun ClassPrinter.getFullClassName(type: Type): String { + return if (type is ClassOrInterfaceType) { + + getFullClassName(buildString { + type.scope.ifPresent { append(it).append(".") } + append(type.nameAsString) + }) + (type.typeArguments.orElse(null)?.let { args -> args.joinToString(",") {getFullClassName(it)}}?.let { "<$it>" } ?: "") + } else getFullClassName(type.asString()) +} + +private fun ClassPrinter.getFullClassName(className: String): String { + if (className.endsWith("[]")) return getFullClassName(className.removeSuffix("[]")) + "[]" + + if (className.matches("\\.[a-z]".toRegex())) return className //qualified name + + if ("." in className) return getFullClassName(className.substringBeforeLast(".")) + "." + className.substringAfterLast(".") + + fileAst.imports.find { imp -> + imp.nameAsString.endsWith(".$className") + }?.nameAsString?.let { return it } + + val thisPackagePrefix = fileAst.packageDeclaration.map { it.nameAsString + "." }.orElse("") + val thisClassPrefix = thisPackagePrefix + classAst.nameAsString + "." + + if (classAst.nameAsString == className) return thisPackagePrefix + classAst.nameAsString + + nestedClasses.find { + it.nameAsString == className + }?.let { return thisClassPrefix + it.nameAsString } + + if (className == CANONICAL_BUILDER_CLASS || className == BASE_BUILDER_CLASS) { + return thisClassPrefix + className + } + + constDefs.find { it.AnnotationName == className }?.let { return thisClassPrefix + className } + + if (tryOrNull { Class.forName("java.lang.$className") } != null) { + return "java.lang.$className" + } + + if (className[0].isLowerCase()) return className //primitive + + return thisPackagePrefix + className +} + +private inline fun <T> tryOrNull(f: () -> T?) = try { + f() +} catch (e: Exception) { + null +} diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt new file mode 100755 index 000000000000..4b508d022991 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/Main.kt @@ -0,0 +1,136 @@ +package com.android.codegen + +import com.github.javaparser.JavaParser +import java.io.File + + +const val THIS_SCRIPT_LOCATION = "" +const val GENERATED_WARNING_PREFIX = "Code below generated by $CODEGEN_NAME" +const val GENERATED_END = "// End of generated code" +const val INDENT_SINGLE = " " + +val PRIMITIVE_TYPES = listOf("byte", "short", "int", "long", "char", "float", "double", "boolean") +val BOXED_PRIMITIVE_TYPES = PRIMITIVE_TYPES.map { it.capitalize() } - "Int" + "Integer" - "Char" + "Character" + +val BUILTIN_SPECIAL_PARCELLINGS = listOf("Pattern") + +const val FLAG_BUILDER_PROTECTED_SETTERS = "--builder-protected-setters" +const val FLAG_NO_FULL_QUALIFIERS = "--no-full-qualifiers" + +val JAVA_PARSER = JavaParser() + +/** @see [FeatureFlag] */ +val USAGE = """ +Usage: $CODEGEN_NAME [--[PREFIX-]FEATURE...] JAVAFILE + +Generates boilerplade parcelable/data class code at the bottom of JAVAFILE, based o fields' declaration in the given JAVAFILE's top-level class + +FEATURE represents some generatable code, and can be among: +${FeatureFlag.values().map { feature -> + " ${feature.kebabCase}" to feature.desc +}.columnize(" - ")} + +And PREFIX can be: + <empty> - request to generate the feature + no - suppress generation of the feature + hidden - request to generate the feature with @hide + +Extra options: + --help - view this help + --update-only - auto-detect flags from the previously auto-generated comment within the file + $FLAG_NO_FULL_QUALIFIERS + - when referring to classes don't use package name prefix; handy with IDE auto-import + $FLAG_BUILDER_PROTECTED_SETTERS + - make builder's setters protected to expose them as public in a subclass on a whitelist basis + + +Special field modifiers and annotations: + transient - ignore the field completely + @Nullable - support null value when parcelling, and never throw on null input + @NonNull - throw on null input and don't parcel the nullness bit for the field + @DataClass.Enum - parcel field as an enum value by ordinal + @DataClass.PluralOf(..) - provide a singular version of a collection field name to be used in the builder's 'addFoo(..)' + @DataClass.ParcelWith(..) - provide a custom Parcelling class, specifying the custom (un)parcelling logic for this field + = <initializer>; - provide default value and never throw if this field was not provided e.g. when using builder + /** ... */ - copy given javadoc on field's getters/setters/constructor params/builder setters etc. + @hide (in javadoc) - force field's getters/setters/withers/builder setters to be @hide-den if generated + + +Special methods/etc. you can define: + + <any auto-generatable method> + For any method to be generated, if a method with same name and argument types is already + defined, than that method will not be generated. + This allows you to override certain details on granular basis. + + void onConstructed() + Will be called in constructor, after all the fields have been initialized. + This is a good place to put any custom validation logic that you may have + + static class $CANONICAL_BUILDER_CLASS extends $BASE_BUILDER_CLASS + If a class extending $BASE_BUILDER_CLASS is specified, generated builder's setters will + return the provided $CANONICAL_BUILDER_CLASS type. + $BASE_BUILDER_CLASS's constructor(s) will be package-private to encourage using $CANONICAL_BUILDER_CLASS instead + This allows you to extend the generated builder, adding or overriding any methods you may want + + +In addition, for any field mMyField(or myField) of type FieldType you can define the following methods: + + void parcelMyField(Parcel dest, int flags) + Allows you to provide custom logic for storing mMyField into a Parcel + + static FieldType unparcelMyField(Parcel in) + Allows you to provide custom logic to deserialize the value of mMyField from a Parcel + + String myFieldToString() + Allows you to provide a custom toString representation of mMyField's value + + FieldType lazyInitMyField() + Requests a lazy initialization in getMyField(), with the provided method being the constructor + You may additionally mark the fields as volatile to cause this to generate a thread-safe + double-check locking lazy initialization + + FieldType defaultMyField() + Allows you to provide a default value to initialize the field to, in case an explicit one + was not provided. + This is an alternative to providing a field initializer that, unlike the initializer, + you can use with final fields. + +Version: $CODEGEN_VERSION + +Questions? Feedback? +Contact: eugenesusla@ +Bug/feature request: http://go/codegen-bug + +Slides: http://go/android-codegen +In-depth example: http://go/SampleDataClass +""" + +fun main(args: Array<String>) { + if (args.contains("--help")) { + println(USAGE) + System.exit(0) + } + if (args.contains("--version")) { + println(CODEGEN_VERSION) + System.exit(0) + } + val file = File(args.last()).absoluteFile + val sourceLisnesOriginal = file.readLines() + val sourceLinesNoClosingBrace = sourceLisnesOriginal.dropLastWhile { + it.startsWith("}") || it.all(Char::isWhitespace) + } + val cliArgs = handleUpdateFlag(args, sourceLinesNoClosingBrace) + + val fileInfo = FileInfo(sourceLisnesOriginal, cliArgs, file) + fileInfo.main() + file.writeText(fileInfo.stringBuilder.toString().mapLines { trimEnd() }) +} + +private fun handleUpdateFlag(cliArgs: Array<String>, sourceLines: List<String>): Array<String> { + if ("--update-only" in cliArgs + && sourceLines.none { GENERATED_WARNING_PREFIX in it || it.startsWith("@DataClass") }) { + System.exit(0) + } + return cliArgs - "--update-only" +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/Printer.kt b/tools/codegen/src/com/android/codegen/Printer.kt new file mode 100644 index 000000000000..b30e3f68b307 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/Printer.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.codegen + +/** + * Mixin for syntactic sugar around indent-aware printing into [stringBuilder] + */ +interface Printer<SELF: Printer<SELF>> { + + val stringBuilder: StringBuilder + + var currentIndent: String + + fun pushIndent() { + currentIndent += INDENT_SINGLE + } + + fun popIndent() { + currentIndent = if (currentIndent.length >= INDENT_SINGLE.length) { + currentIndent.substring(0, currentIndent.length - INDENT_SINGLE.length) + } else { + "" + } + } + + fun backspace() = stringBuilder.setLength(stringBuilder.length - 1) + val lastChar get() = stringBuilder[stringBuilder.length - 1] + + private fun appendRaw(s: String) { + stringBuilder.append(s) + } + + fun append(s: String) { + if (s.isBlank() && s != "\n") { + appendRaw(s) + } else { + appendRaw(s.lines().map { line -> + if (line.startsWith(" *")) line else line.trimStart() + }.joinToString("\n$currentIndent")) + } + } + + fun appendSameLine(s: String) { + while (lastChar.isWhitespace() || lastChar.isNewline()) { + backspace() + } + appendRaw(s) + } + + fun rmEmptyLine() { + while (lastChar.isWhitespaceNonNewline()) backspace() + if (lastChar.isNewline()) backspace() + } + + /** + * Syntactic sugar for: + * ``` + * +"code()"; + * ``` + * to append the given string plus a newline + */ + operator fun String.unaryPlus() = append("$this\n") + + /** + * Syntactic sugar for: + * ``` + * !"code()"; + * ``` + * to append the given string without a newline + */ + operator fun String.not() = append(this) + + /** + * Syntactic sugar for: + * ``` + * ... { + * ... + * }+";" + * ``` + * to append a ';' on same line after a block, and a newline afterwards + */ + operator fun Unit.plus(s: String) { + appendSameLine(s) + +"" + } + + /** + * A multi-purpose syntactic sugar for appending the given string plus anything generated in + * the given [block], the latter with the appropriate deeper indent, + * and resetting the indent back to original at the end + * + * Usage examples: + * + * ``` + * "if (...)" { + * ... + * } + * ``` + * to append a corresponding if block appropriate indentation + * + * ``` + * "void foo(...)" { + * ... + * } + * ``` + * similar to the previous one, plus an extra empty line after the function body + * + * ``` + * "void foo(" { + * <args code> + * } + * ``` + * to use proper indentation for args code and close the bracket on same line at end + * + * ``` + * "..." { + * ... + * } + * to use the correct indentation for inner code, resetting it at the end + */ + operator fun String.invoke(block: SELF.() -> Unit) { + if (this == " {") { + appendSameLine(this) + } else { + append(this) + } + when { + endsWith("(") -> { + indentedBy(2, block) + appendSameLine(")") + } + endsWith("{") || endsWith(")") -> { + if (!endsWith("{")) appendSameLine(" {") + indentedBy(1, block) + +"}" + if ((endsWith(") {") || endsWith(")") || this == " {") + && !startsWith("synchronized") + && !startsWith("switch") + && !startsWith("if ") + && !contains(" else ") + && !contains("new ") + && !contains("return ")) { + +"" // extra line after function definitions + } + } + else -> indentedBy(2, block) + } + } + + fun indentedBy(level: Int, block: SELF.() -> Unit) { + append("\n") + level times { + append(INDENT_SINGLE) + pushIndent() + } + (this as SELF).block() + level times { popIndent() } + rmEmptyLine() + +"" + } + + fun Iterable<FieldInfo>.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) { + forEachApply { + b() + if (isLast) { + while (lastChar == ' ' || lastChar == '\n') backspace() + if (lastChar == ',') backspace() + } + } + } +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/SharedConstants.kt b/tools/codegen/src/com/android/codegen/SharedConstants.kt new file mode 100644 index 000000000000..6f740cd663e3 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/SharedConstants.kt @@ -0,0 +1,7 @@ +package com.android.codegen + +const val CODEGEN_NAME = "codegen" +const val CODEGEN_VERSION = "1.0.15" + +const val CANONICAL_BUILDER_CLASS = "Builder" +const val BASE_BUILDER_CLASS = "BaseBuilder" diff --git a/tools/codegen/src/com/android/codegen/Utils.kt b/tools/codegen/src/com/android/codegen/Utils.kt new file mode 100644 index 000000000000..c19ae3b0b11f --- /dev/null +++ b/tools/codegen/src/com/android/codegen/Utils.kt @@ -0,0 +1,146 @@ +package com.android.codegen + +import com.github.javaparser.JavaParser +import com.github.javaparser.ParseProblemException +import com.github.javaparser.ParseResult +import com.github.javaparser.ast.Node +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.body.TypeDeclaration +import com.github.javaparser.ast.expr.* +import com.github.javaparser.ast.nodeTypes.NodeWithModifiers +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +/** + * [Iterable.forEach] + [Any.apply] + */ +inline fun <T> Iterable<T>.forEachApply(block: T.() -> Unit) = forEach(block) + +inline fun String.mapLines(f: String.() -> String?) = lines().mapNotNull(f).joinToString("\n") +inline fun <T> Iterable<T>.trim(f: T.() -> Boolean) = dropWhile(f).dropLastWhile(f) +fun String.trimBlankLines() = lines().trim { isBlank() }.joinToString("\n") + +fun Char.isNewline() = this == '\n' || this == '\r' +fun Char.isWhitespaceNonNewline() = isWhitespace() && !isNewline() + +fun if_(cond: Boolean, then: String) = if (cond) then else "" + +fun <T> Any?.as_(): T = this as T + +inline infix fun Int.times(action: () -> Unit) { + for (i in 1..this) action() +} + +/** + * a bbb + * cccc dd + * + * -> + * + * a bbb + * cccc dd + */ +fun Iterable<Pair<String, String>>.columnize(separator: String = " | "): String { + val col1w = map { (a, _) -> a.length }.max()!! + val col2w = map { (_, b) -> b.length }.max()!! + return map { it.first.padEnd(col1w) + separator + it.second.padEnd(col2w) }.joinToString("\n") +} + +fun String.hasUnbalancedCurlyBrace(): Boolean { + var braces = 0 + forEach { + if (it == '{') braces++ + if (it == '}') braces-- + if (braces < 0) return true + } + return false +} + +fun String.toLowerCamel(): String { + if (length >= 2 && this[0] == 'm' && this[1].isUpperCase()) return substring(1).capitalize() + if (all { it.isLetterOrDigit() }) return decapitalize() + return split("[^a-zA-Z0-9]".toRegex()) + .map { it.toLowerCase().capitalize() } + .joinToString("") + .decapitalize() +} + +inline fun <T> List<T>.forEachLastAware(f: (T, Boolean) -> Unit) { + forEachIndexed { index, t -> f(t, index == size - 1) } +} + +@Suppress("UNCHECKED_CAST") +fun <T : Expression> AnnotationExpr.singleArgAs() + = ((this as SingleMemberAnnotationExpr).memberValue as T) + +inline operator fun <reified T> Array<T>.minus(item: T) = toList().minus(item).toTypedArray() + +fun currentTimestamp() = DateTimeFormatter + .ofLocalizedDateTime(/* date */ FormatStyle.MEDIUM, /* time */ FormatStyle.LONG) + .withZone(ZoneId.systemDefault()) + .format(Instant.now()) + +val NodeWithModifiers<*>.visibility get() = accessSpecifier + +fun abort(msg: String): Nothing { + System.err.println("ERROR: $msg") + System.exit(1) + throw InternalError() // can't get here +} + +fun bitAtExpr(bitIndex: Int) = "0x${java.lang.Long.toHexString(1L shl bitIndex)}" + +val AnnotationExpr.args: Map<String, Expression> get() = when (this) { + is MarkerAnnotationExpr -> emptyMap() + is SingleMemberAnnotationExpr -> mapOf("value" to memberValue) + is NormalAnnotationExpr -> pairs.map { it.name.asString() to it.value }.toMap() + else -> throw IllegalArgumentException("Unknown annotation expression: $this") +} + +val TypeDeclaration<*>.nestedTypes get() = childNodes.filterIsInstance<TypeDeclaration<*>>() +val TypeDeclaration<*>.nestedDataClasses get() + = nestedTypes.filterIsInstance<ClassOrInterfaceDeclaration>() + .filter { it.annotations.any { it.nameAsString.endsWith("DataClass") } } +val TypeDeclaration<*>.startLine get() = range.get()!!.begin.line + +inline fun <T> List<T>.forEachSequentialPair(action: (T, T?) -> Unit) { + forEachIndexed { index, t -> + action(t, getOrNull(index + 1)) + } +} + +fun <T: Node> parseJava(fn: JavaParser.(String) -> ParseResult<T>, source: String): T = try { + val parse = JAVA_PARSER.fn(source) + if (parse.problems.isNotEmpty()) { + throw parseFailed( + source, + desc = parse.problems.joinToString("\n"), + cause = parse.problems.mapNotNull { it.cause.orElse(null) }.firstOrNull()) + } + parse.result.get() +} catch (e: ParseProblemException) { + throw parseFailed(source, cause = e) +} + +private fun parseFailed(source: String, cause: Throwable? = null, desc: String = ""): RuntimeException { + return RuntimeException("Failed to parse code:\n" + + source + .lines() + .mapIndexed { lnNum, ln -> "/*$lnNum*/$ln" } + .joinToString("\n") + "\n$desc", + cause) +} + +var <T> MutableList<T>.last + get() = last() + set(value) { + if (isEmpty()) { + add(value) + } else { + this[size - 1] = value + } + } + +inline fun <T> buildList(init: MutableList<T>.() -> Unit) = mutableListOf<T>().apply(init)
\ No newline at end of file diff --git a/tools/incident_section_gen/main.cpp b/tools/incident_section_gen/main.cpp index ba1267b28fbb..dd266ffe0f16 100644 --- a/tools/incident_section_gen/main.cpp +++ b/tools/incident_section_gen/main.cpp @@ -418,7 +418,7 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { } const SectionFlags s = getSectionFlags(field); - if (s.userdebug_and_eng_only()) { + if (s.userdebug_and_eng_only() || s.type() == SECTION_TEXT_DUMPSYS) { printf("#if ALLOW_RESTRICTED_SECTIONS\n"); } @@ -439,7 +439,9 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { printf(" NULL),\n"); break; case SECTION_LOG: - printf(" new LogSection(%d, %s),\n", field->number(), s.args().c_str()); + printf(" new LogSection(%d, ", field->number()); + splitAndPrint(s.args()); + printf(" NULL),\n"); break; case SECTION_GZIP: printf(" new GZipSection(%d,", field->number()); @@ -450,8 +452,13 @@ static bool generateSectionListCpp(Descriptor const* descriptor) { printf(" new TombstoneSection(%d, \"%s\"),\n", field->number(), s.args().c_str()); break; + case SECTION_TEXT_DUMPSYS: + printf(" new TextDumpsysSection(%d, ", field->number()); + splitAndPrint(s.args()); + printf(" NULL),\n"); + break; } - if (s.userdebug_and_eng_only()) { + if (s.userdebug_and_eng_only() || s.type() == SECTION_TEXT_DUMPSYS) { printf("#endif\n"); } } diff --git a/tools/processors/staledataclass/Android.bp b/tools/processors/staledataclass/Android.bp new file mode 100644 index 000000000000..58a7d346ce1f --- /dev/null +++ b/tools/processors/staledataclass/Android.bp @@ -0,0 +1,29 @@ + +java_plugin { + name: "staledataclass-annotation-processor", + processor_class: "android.processor.staledataclass.StaleDataclassProcessor", + + java_resources: [ + "META-INF/**/*", + ], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "codegen-version-info", + ], + // The --add-modules/exports flags below don't work for kotlinc yet, so pin this module to Java language level 8 (see b/139342589): + java_version: "1.8", + openjdk9: { + javacflags: [ + "--add-modules=jdk.compiler", + "--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + ], + }, + + use_tools_jar: true, +} diff --git a/tools/processors/staledataclass/META-INF/services/javax.annotation.processing.Processor b/tools/processors/staledataclass/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000000..15ee6230c023 --- /dev/null +++ b/tools/processors/staledataclass/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +android.processor.staledataclass.StaleDataclassProcessorOld diff --git a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt new file mode 100644 index 000000000000..51faa49a86cc --- /dev/null +++ b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.processor.staledataclass + +import com.android.codegen.BASE_BUILDER_CLASS +import com.android.codegen.CANONICAL_BUILDER_CLASS +import com.android.codegen.CODEGEN_NAME +import com.android.codegen.CODEGEN_VERSION +import com.sun.tools.javac.code.Symbol +import com.sun.tools.javac.code.Type +import java.io.File +import java.io.FileNotFoundException +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.RoundEnvironment +import javax.annotation.processing.SupportedAnnotationTypes +import javax.lang.model.SourceVersion +import javax.lang.model.element.AnnotationMirror +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.TypeElement +import javax.tools.Diagnostic + +private const val STALE_FILE_THRESHOLD_MS = 1000 +private val WORKING_DIR = File(".").absoluteFile + +private const val DATACLASS_ANNOTATION_NAME = "com.android.internal.util.DataClass" +private const val GENERATED_ANNOTATION_NAME = "com.android.internal.util.DataClass.Generated" +private const val GENERATED_MEMBER_ANNOTATION_NAME + = "com.android.internal.util.DataClass.Generated.Member" + + +@SupportedAnnotationTypes(DATACLASS_ANNOTATION_NAME, GENERATED_ANNOTATION_NAME) +class StaleDataclassProcessor: AbstractProcessor() { + + private var dataClassAnnotation: TypeElement? = null + private var generatedAnnotation: TypeElement? = null + private var repoRoot: File? = null + + private val stale = mutableListOf<Stale>() + + /** + * This is the main entry point in the processor, called by the compiler. + */ + override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean { + + if (generatedAnnotation == null) { + generatedAnnotation = annotations.find { + it.qualifiedName.toString() == GENERATED_ANNOTATION_NAME + } + } + if (dataClassAnnotation == null) { + dataClassAnnotation = annotations.find { + it.qualifiedName.toString() == DATACLASS_ANNOTATION_NAME + } ?: return true + } + + val generatedAnnotatedElements = if (generatedAnnotation != null) { + roundEnv.getElementsAnnotatedWith(generatedAnnotation) + } else { + emptySet() + } + generatedAnnotatedElements.forEach { + processSingleFile(it) + } + + + val dataClassesWithoutGeneratedPart = + roundEnv.getElementsAnnotatedWith(dataClassAnnotation) - + generatedAnnotatedElements.map { it.enclosingElement } + + dataClassesWithoutGeneratedPart.forEach { dataClass -> + stale += Stale(dataClass.toString(), file = null, lastGenerated = 0L) + } + + + if (!stale.isEmpty()) { + error("Stale generated dataclass(es) detected. " + + "Run the following command(s) to update them:" + + stale.joinToString("") { "\n" + it.refreshCmd }) + } + return true + } + + private fun elemToString(elem: Element): String { + return buildString { + append(elem.modifiers.joinToString(" ") { it.name.toLowerCase() }).append(" ") + append(elem.annotationMirrors.joinToString(" ")).append(" ") + if (elem is Symbol) { + if (elem.type is Type.MethodType) { + append((elem.type as Type.MethodType).returnType) + } else { + append(elem.type) + } + append(" ") + } + append(elem) + } + } + + private fun processSingleFile(elementAnnotatedWithGenerated: Element) { + + val classElement = elementAnnotatedWithGenerated.enclosingElement + + val inputSignatures = computeSignaturesForClass(classElement) + .plus(computeSignaturesForClass(classElement.enclosedElements.find { + it.kind == ElementKind.CLASS + && !isGenerated(it) + && it.simpleName.toString() == BASE_BUILDER_CLASS + })) + .plus(computeSignaturesForClass(classElement.enclosedElements.find { + it.kind == ElementKind.CLASS + && !isGenerated(it) + && it.simpleName.toString() == CANONICAL_BUILDER_CLASS + })) + .plus(classElement + .annotationMirrors + .find { it.annotationType.toString() == DATACLASS_ANNOTATION_NAME } + .toString()) + .toSet() + + val annotationParams = elementAnnotatedWithGenerated + .annotationMirrors + .find { ann -> isGeneratedAnnotation(ann) }!! + .elementValues + .map { (k, v) -> k.simpleName.toString() to v.value } + .toMap() + + val lastGenerated = annotationParams["time"] as Long + val codegenVersion = annotationParams["codegenVersion"] as String + val codegenMajorVersion = codegenVersion.substringBefore(".") + val sourceRelative = File(annotationParams["sourceFile"] as String) + + val lastGenInputSignatures = (annotationParams["inputSignatures"] as String).lines().toSet() + + if (repoRoot == null) { + repoRoot = generateSequence(WORKING_DIR) { it.parentFile } + .find { it.resolve(sourceRelative).isFile } + ?.canonicalFile + ?: throw FileNotFoundException( + "Failed to detect repository root: " + + "no parent of $WORKING_DIR contains $sourceRelative") + } + + val source = repoRoot!!.resolve(sourceRelative) + val clazz = classElement.toString() + + if (inputSignatures != lastGenInputSignatures) { + error(buildString { + append(sourceRelative).append(":\n") + append(" Added:\n").append((inputSignatures-lastGenInputSignatures).joinToString("\n")) + append("\n") + append(" Removed:\n").append((lastGenInputSignatures-inputSignatures).joinToString("\n")) + }) + stale += Stale(clazz, source, lastGenerated) + } + + if (codegenMajorVersion != CODEGEN_VERSION.substringBefore(".")) { + stale += Stale(clazz, source, lastGenerated) + } + } + + private fun computeSignaturesForClass(classElement: Element?): List<String> { + if (classElement == null) return emptyList() + val type = classElement as TypeElement + return classElement + .enclosedElements + .filterNot { + it.kind == ElementKind.CLASS + || it.kind == ElementKind.CONSTRUCTOR + || it.kind == ElementKind.INTERFACE + || it.kind == ElementKind.ENUM + || it.kind == ElementKind.ANNOTATION_TYPE + || it.kind == ElementKind.INSTANCE_INIT + || it.kind == ElementKind.STATIC_INIT + || isGenerated(it) + }.map { + elemToString(it) + } + "class ${classElement.simpleName} extends ${type.superclass} implements [${type.interfaces.joinToString(", ")}]" + } + + private fun isGenerated(it: Element) = + it.annotationMirrors.any { "Generated" in it.annotationType.toString() } + + private fun error(msg: String) { + processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg) + } + + private fun isGeneratedAnnotation(ann: AnnotationMirror): Boolean { + return generatedAnnotation!!.qualifiedName.toString() == ann.annotationType.toString() + } + + data class Stale(val clazz: String, val file: File?, val lastGenerated: Long) { + val refreshCmd = if (file != null) { + "$CODEGEN_NAME $file" + } else { + var gotTopLevelCalssName = false + val filePath = clazz.split(".") + .takeWhile { word -> + if (!gotTopLevelCalssName && word[0].isUpperCase()) { + gotTopLevelCalssName = true + return@takeWhile true + } + !gotTopLevelCalssName + }.joinToString("/") + "find \$ANDROID_BUILD_TOP -path */$filePath.java -exec $CODEGEN_NAME {} \\;" + } + } + + override fun getSupportedSourceVersion(): SourceVersion { + return SourceVersion.latest() + } +}
\ No newline at end of file diff --git a/tools/protologtool/Android.bp b/tools/protologtool/Android.bp new file mode 100644 index 000000000000..ce551bd0cc10 --- /dev/null +++ b/tools/protologtool/Android.bp @@ -0,0 +1,33 @@ +java_library_host { + name: "protologtool-lib", + srcs: [ + "src/com/android/protolog/tool/**/*.kt", + ], + static_libs: [ + "protolog-common", + "javaparser", + "platformprotos", + "jsonlib", + ], +} + +java_binary_host { + name: "protologtool", + manifest: "manifest.txt", + static_libs: [ + "protologtool-lib", + ], +} + +java_test_host { + name: "protologtool-tests", + test_suites: ["general-tests"], + srcs: [ + "tests/**/*.kt", + ], + static_libs: [ + "protologtool-lib", + "junit", + "mockito", + ], +} diff --git a/tools/protologtool/README.md b/tools/protologtool/README.md new file mode 100644 index 000000000000..ba639570f88c --- /dev/null +++ b/tools/protologtool/README.md @@ -0,0 +1,119 @@ +# ProtoLogTool + +Code transformation tool and viewer for ProtoLog. + +## What does it do? + +ProtoLogTool incorporates three different modes of operation: + +### Code transformation + +Command: `protologtool transform-protolog-calls + --protolog-class <protolog class name> + --protolog-impl-class <protolog implementation class name> + --loggroups-class <protolog groups class name> + --loggroups-jar <config jar path> + --output-srcjar <output.srcjar> + [<input.java>]` + +In this mode ProtoLogTool transforms every ProtoLog logging call in form of: +```java +ProtoLog.x(ProtoLogGroup.GROUP_NAME, "Format string %d %s", value1, value2); +``` +into: +```java +if (ProtoLogImpl.isEnabled(GROUP_NAME)) { + int protoLogParam0 = value1; + String protoLogParam1 = String.valueOf(value2); + ProtoLogImpl.x(ProtoLogGroup.GROUP_NAME, 123456, 0b0100, "Format string %d %s or null", protoLogParam0, protoLogParam1); +} +``` +where `ProtoLog`, `ProtoLogImpl` and `ProtoLogGroup` are the classes provided as arguments + (can be imported, static imported or full path, wildcard imports are not allowed) and, `x` is the + logging method. The transformation is done on the source level. A hash is generated from the format + string, log level and log group name and inserted after the `ProtoLogGroup` argument. After the hash + we insert a bitmask specifying the types of logged parameters. The format string is replaced + by `null` if `ProtoLogGroup.GROUP_NAME.isLogToLogcat()` returns false. If `ProtoLogGroup.GROUP_NAME.isEnabled()` + returns false the log statement is removed entirely from the resultant code. The real generated code is inlined + and a number of new line characters is added as to preserve line numbering in file. + +Input is provided as a list of java source file names. Transformed source is saved to a single +source jar file. The ProtoLogGroup class with all dependencies should be provided as a compiled +jar file (config.jar). + +### Viewer config generation + +Command: `generate-viewer-config + --protolog-class <protolog class name> + --loggroups-class <protolog groups class name> + --loggroups-jar <config jar path> + --viewer-conf <viewer.json> + [<input.java>]` + +This command is similar in it's syntax to the previous one, only instead of creating a processed source jar +it writes a viewer configuration file with following schema: +```json +{ + "version": "1.0.0", + "messages": { + "123456": { + "message": "Format string %d %s", + "level": "ERROR", + "group": "GROUP_NAME", + "at": "com\/android\/server\/example\/Class.java" + } + }, + "groups": { + "GROUP_NAME": { + "tag": "TestLog" + } + } +} + +``` + +### Binary log viewing + +Command: `read-log --viewer-conf <viewer.json> <wm_log.pb>` + +Reads the binary ProtoLog log file and outputs a human-readable LogCat-like text log. + +## What is ProtoLog? + +ProtoLog is a generic logging system created for the WindowManager project. It allows both binary and text logging +and is tunable in runtime. It consists of 3 different submodules: +* logging system built-in the Android app, +* log viewer for reading binary logs, +* a code processing tool. + +ProtoLog is designed to reduce both application size (and by that memory usage) and amount of resources needed +for logging. This is achieved by replacing log message strings with their hashes and only loading to memory/writing +full log messages when necessary. + +### Text logging + +For text-based logs Android LogCat is used as a backend. Message strings are loaded from a viewer config +located on the device when needed. + +### Binary logging + +Binary logs are saved as Protocol Buffers file. They can be read using the ProtoLog tool or specialised +viewer like Winscope. + +## How to use ProtoLog? + +### Adding a new logging group or log statement + +To add a new ProtoLogGroup simple create a new enum ProtoLogGroup member with desired parameters. + +To add a new logging statement just add a new call to ProtoLog.x where x is a log level. + +After doing any changes to logging groups or statements you should build the project and follow instructions printed by the tool. + +## How to change settings on device in runtime? +Use the `adb shell su root cmd window logging` command. To get help just type +`adb shell su root cmd window logging help`. + + + + diff --git a/tools/protologtool/TEST_MAPPING b/tools/protologtool/TEST_MAPPING new file mode 100644 index 000000000000..52b12dc26be9 --- /dev/null +++ b/tools/protologtool/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "protologtool-tests" + } + ] +} diff --git a/tools/protologtool/manifest.txt b/tools/protologtool/manifest.txt new file mode 100644 index 000000000000..cabebd51a2fa --- /dev/null +++ b/tools/protologtool/manifest.txt @@ -0,0 +1 @@ +Main-class: com.android.protolog.tool.ProtoLogTool diff --git a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt new file mode 100644 index 000000000000..a52c8042582b --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.ImportDeclaration +import com.github.javaparser.ast.expr.BinaryExpr +import com.github.javaparser.ast.expr.Expression +import com.github.javaparser.ast.expr.StringLiteralExpr + +object CodeUtils { + /** + * Returns a stable hash of a string. + * We reimplement String::hashCode() for readability reasons. + */ + fun hash(position: String, messageString: String, logLevel: LogLevel, logGroup: LogGroup): Int { + return (position + messageString + logLevel.name + logGroup.name) + .map { c -> c.toInt() }.reduce { h, c -> h * 31 + c } + } + + fun checkWildcardStaticImported(code: CompilationUnit, className: String, fileName: String) { + code.findAll(ImportDeclaration::class.java) + .forEach { im -> + if (im.isStatic && im.isAsterisk && im.name.toString() == className) { + throw IllegalImportException("Wildcard static imports of $className " + + "methods are not supported.", ParsingContext(fileName, im)) + } + } + } + + fun isClassImportedOrSamePackage(code: CompilationUnit, className: String): Boolean { + val packageName = className.substringBeforeLast('.') + return code.packageDeclaration.isPresent && + code.packageDeclaration.get().nameAsString == packageName || + code.findAll(ImportDeclaration::class.java) + .any { im -> + !im.isStatic && + ((!im.isAsterisk && im.name.toString() == className) || + (im.isAsterisk && im.name.toString() == packageName)) + } + } + + fun staticallyImportedMethods(code: CompilationUnit, className: String): Set<String> { + return code.findAll(ImportDeclaration::class.java) + .filter { im -> + im.isStatic && + im.name.toString().substringBeforeLast('.') == className + } + .map { im -> im.name.toString().substringAfterLast('.') }.toSet() + } + + fun concatMultilineString(expr: Expression, context: ParsingContext): String { + return when (expr) { + is StringLiteralExpr -> expr.asString() + is BinaryExpr -> when { + expr.operator == BinaryExpr.Operator.PLUS -> + concatMultilineString(expr.left, context) + + concatMultilineString(expr.right, context) + else -> throw InvalidProtoLogCallException( + "expected a string literal " + + "or concatenation of string literals, got: $expr", context) + } + else -> throw InvalidProtoLogCallException("expected a string literal " + + "or concatenation of string literals, got: $expr", context) + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt new file mode 100644 index 000000000000..bfbbf7a32c22 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import java.util.regex.Pattern + +class CommandOptions(args: Array<String>) { + companion object { + const val TRANSFORM_CALLS_CMD = "transform-protolog-calls" + const val GENERATE_CONFIG_CMD = "generate-viewer-config" + const val READ_LOG_CMD = "read-log" + private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD) + + private const val PROTOLOG_CLASS_PARAM = "--protolog-class" + private const val PROTOLOGIMPL_CLASS_PARAM = "--protolog-impl-class" + private const val PROTOLOGCACHE_CLASS_PARAM = "--protolog-cache-class" + private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class" + private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar" + private const val VIEWER_CONFIG_JSON_PARAM = "--viewer-conf" + private const val OUTPUT_SOURCE_JAR_PARAM = "--output-srcjar" + private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGIMPL_CLASS_PARAM, + PROTOLOGCACHE_CLASS_PARAM, PROTOLOGGROUP_CLASS_PARAM, PROTOLOGGROUP_JAR_PARAM, + VIEWER_CONFIG_JSON_PARAM, OUTPUT_SOURCE_JAR_PARAM) + + val USAGE = """ + Usage: ${Constants.NAME} <command> [<args>] + Available commands: + + $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGIMPL_CLASS_PARAM + <class name> $PROTOLOGCACHE_CLASS_PARAM + <class name> $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM + <config.jar> $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>] + - processes java files replacing stub calls with logging code. + + $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGGROUP_CLASS_PARAM + <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar> $VIEWER_CONFIG_JSON_PARAM + <viewer.json> [<input.java>] + - creates viewer config file from given java files. + + $READ_LOG_CMD $VIEWER_CONFIG_JSON_PARAM <viewer.json> <wm_log.pb> + - translates a binary log to a readable format. + """.trimIndent() + + private fun validateClassName(name: String): String { + if (!Pattern.matches("^([a-z]+[A-Za-z0-9]*\\.)+([A-Za-z0-9$]+)$", name)) { + throw InvalidCommandException("Invalid class name $name") + } + return name + } + + private fun getParam(paramName: String, params: Map<String, String>): String { + if (!params.containsKey(paramName)) { + throw InvalidCommandException("Param $paramName required") + } + return params.getValue(paramName) + } + + private fun validateNotSpecified(paramName: String, params: Map<String, String>): String { + if (params.containsKey(paramName)) { + throw InvalidCommandException("Unsupported param $paramName") + } + return "" + } + + private fun validateJarName(name: String): String { + if (!name.endsWith(".jar")) { + throw InvalidCommandException("Jar file required, got $name instead") + } + return name + } + + private fun validateSrcJarName(name: String): String { + if (!name.endsWith(".srcjar")) { + throw InvalidCommandException("Source jar file required, got $name instead") + } + return name + } + + private fun validateJSONName(name: String): String { + if (!name.endsWith(".json")) { + throw InvalidCommandException("Json file required, got $name instead") + } + return name + } + + private fun validateJavaInputList(list: List<String>): List<String> { + if (list.isEmpty()) { + throw InvalidCommandException("No java source input files") + } + list.forEach { name -> + if (!name.endsWith(".java")) { + throw InvalidCommandException("Not a java source file $name") + } + } + return list + } + + private fun validateLogInputList(list: List<String>): String { + if (list.isEmpty()) { + throw InvalidCommandException("No log input file") + } + if (list.size > 1) { + throw InvalidCommandException("Only one log input file allowed") + } + return list[0] + } + } + + val protoLogClassNameArg: String + val protoLogGroupsClassNameArg: String + val protoLogImplClassNameArg: String + val protoLogCacheClassNameArg: String + val protoLogGroupsJarArg: String + val viewerConfigJsonArg: String + val outputSourceJarArg: String + val logProtofileArg: String + val javaSourceArgs: List<String> + val command: String + + init { + if (args.isEmpty()) { + throw InvalidCommandException("No command specified.") + } + command = args[0] + if (command !in commands) { + throw InvalidCommandException("Unknown command.") + } + + val params: MutableMap<String, String> = mutableMapOf() + val inputFiles: MutableList<String> = mutableListOf() + + var idx = 1 + while (idx < args.size) { + if (args[idx].startsWith("--")) { + if (idx + 1 >= args.size) { + throw InvalidCommandException("No value for ${args[idx]}") + } + if (args[idx] !in parameters) { + throw InvalidCommandException("Unknown parameter ${args[idx]}") + } + if (args[idx + 1].startsWith("--")) { + throw InvalidCommandException("No value for ${args[idx]}") + } + if (params.containsKey(args[idx])) { + throw InvalidCommandException("Duplicated parameter ${args[idx]}") + } + params[args[idx]] = args[idx + 1] + idx += 2 + } else { + inputFiles.add(args[idx]) + idx += 1 + } + } + + when (command) { + TRANSFORM_CALLS_CMD -> { + protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params)) + protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, + params)) + protoLogImplClassNameArg = validateClassName(getParam(PROTOLOGIMPL_CLASS_PARAM, + params)) + protoLogCacheClassNameArg = validateClassName(getParam(PROTOLOGCACHE_CLASS_PARAM, + params)) + protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params)) + viewerConfigJsonArg = validateNotSpecified(VIEWER_CONFIG_JSON_PARAM, params) + outputSourceJarArg = validateSrcJarName(getParam(OUTPUT_SOURCE_JAR_PARAM, params)) + javaSourceArgs = validateJavaInputList(inputFiles) + logProtofileArg = "" + } + GENERATE_CONFIG_CMD -> { + protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params)) + protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, + params)) + protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params) + protoLogCacheClassNameArg = validateNotSpecified(PROTOLOGCACHE_CLASS_PARAM, params) + protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params)) + viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params)) + outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params) + javaSourceArgs = validateJavaInputList(inputFiles) + logProtofileArg = "" + } + READ_LOG_CMD -> { + protoLogClassNameArg = validateNotSpecified(PROTOLOG_CLASS_PARAM, params) + protoLogGroupsClassNameArg = validateNotSpecified(PROTOLOGGROUP_CLASS_PARAM, params) + protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params) + protoLogCacheClassNameArg = validateNotSpecified(PROTOLOGCACHE_CLASS_PARAM, params) + protoLogGroupsJarArg = validateNotSpecified(PROTOLOGGROUP_JAR_PARAM, params) + viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params)) + outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params) + javaSourceArgs = listOf() + logProtofileArg = validateLogInputList(inputFiles) + } + else -> { + throw InvalidCommandException("Unknown command.") + } + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/Constants.kt b/tools/protologtool/src/com/android/protolog/tool/Constants.kt new file mode 100644 index 000000000000..aa3e00f2f4db --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/Constants.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +object Constants { + const val NAME = "protologtool" + const val VERSION = "1.0.0" + const val IS_ENABLED_METHOD = "isEnabled" + const val ENUM_VALUES_METHOD = "values" +} diff --git a/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt b/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt new file mode 100644 index 000000000000..587f7b9db016 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +data class LogGroup( + val name: String, + val enabled: Boolean, + val textEnabled: Boolean, + val tag: String +) diff --git a/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt b/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt new file mode 100644 index 000000000000..e88f0f8231bd --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.Node + +enum class LogLevel { + DEBUG, VERBOSE, INFO, WARN, ERROR, WTF; + + companion object { + fun getLevelForMethodName(name: String, node: Node, context: ParsingContext): LogLevel { + return when (name) { + "d" -> DEBUG + "v" -> VERBOSE + "i" -> INFO + "w" -> WARN + "e" -> ERROR + "wtf" -> WTF + else -> + throw InvalidProtoLogCallException("Unknown log level $name in $node", context) + } + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/LogParser.kt b/tools/protologtool/src/com/android/protolog/tool/LogParser.kt new file mode 100644 index 000000000000..a59038fc99a0 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/LogParser.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import com.android.server.protolog.common.InvalidFormatStringException +import com.android.server.protolog.common.LogDataType +import com.android.server.protolog.ProtoLogMessage +import com.android.server.protolog.ProtoLogFileProto +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.io.PrintStream +import java.lang.Exception +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Implements a simple parser/viewer for binary ProtoLog logs. + * A binary log is translated into Android "LogCat"-like text log. + */ +class LogParser(private val configParser: ViewerConfigParser) { + companion object { + private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) + private val magicNumber = + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + } + + private fun printTime(time: Long, offset: Long, ps: PrintStream) { + ps.print(dateFormat.format(Date(time / 1000000 + offset)) + " ") + } + + private fun printFormatted( + protoLogMessage: ProtoLogMessage, + configEntry: ViewerConfigParser.ConfigEntry, + ps: PrintStream + ) { + val strParmIt = protoLogMessage.strParamsList.iterator() + val longParamsIt = protoLogMessage.sint64ParamsList.iterator() + val doubleParamsIt = protoLogMessage.doubleParamsList.iterator() + val boolParamsIt = protoLogMessage.booleanParamsList.iterator() + val args = mutableListOf<Any>() + val format = configEntry.messageString + val argTypes = LogDataType.parseFormatString(format) + try { + argTypes.forEach { + when (it) { + LogDataType.BOOLEAN -> args.add(boolParamsIt.next()) + LogDataType.LONG -> args.add(longParamsIt.next()) + LogDataType.DOUBLE -> args.add(doubleParamsIt.next()) + LogDataType.STRING -> args.add(strParmIt.next()) + null -> throw NullPointerException() + } + } + } catch (ex: NoSuchElementException) { + throw InvalidFormatStringException("Invalid format string in config", ex) + } + if (strParmIt.hasNext() || longParamsIt.hasNext() || + doubleParamsIt.hasNext() || boolParamsIt.hasNext()) { + throw RuntimeException("Invalid format string in config - no enough matchers") + } + val formatted = format.format(*(args.toTypedArray())) + ps.print("${configEntry.level} ${configEntry.tag}: $formatted\n") + } + + private fun printUnformatted(protoLogMessage: ProtoLogMessage, ps: PrintStream, tag: String) { + ps.println("$tag: ${protoLogMessage.messageHash} - ${protoLogMessage.strParamsList}" + + " ${protoLogMessage.sint64ParamsList} ${protoLogMessage.doubleParamsList}" + + " ${protoLogMessage.booleanParamsList}") + } + + fun parse(protoLogInput: InputStream, jsonConfigInput: InputStream, ps: PrintStream) { + val jsonReader = JsonReader(BufferedReader(InputStreamReader(jsonConfigInput))) + val config = configParser.parseConfig(jsonReader) + val protoLog = ProtoLogFileProto.parseFrom(protoLogInput) + + if (protoLog.magicNumber != magicNumber) { + throw InvalidInputException("ProtoLog file magic number is invalid.") + } + if (protoLog.version != Constants.VERSION) { + throw InvalidInputException("ProtoLog file version not supported by this tool," + + " log version ${protoLog.version}, viewer version ${Constants.VERSION}") + } + + protoLog.logList.forEach { log -> + printTime(log.elapsedRealtimeNanos, protoLog.realTimeToElapsedTimeOffsetMillis, ps) + if (log.messageHash !in config) { + printUnformatted(log, ps, "UNKNOWN") + } else { + val conf = config.getValue(log.messageHash) + try { + printFormatted(log, conf, ps) + } catch (ex: Exception) { + printUnformatted(log, ps, "INVALID") + } + } + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt b/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt new file mode 100644 index 000000000000..c6aedfc3093e --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.Node + +data class ParsingContext(val filePath: String, val lineNumber: Int) { + constructor(filePath: String, node: Node) + : this(filePath, if (node.range.isPresent) node.range.get().begin.line else -1) + + constructor() : this("", -1) +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt new file mode 100644 index 000000000000..2181cf680f6c --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.expr.Expression +import com.github.javaparser.ast.expr.FieldAccessExpr +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.expr.NameExpr + +/** + * Helper class for visiting all ProtoLog calls. + * For every valid call in the given {@code CompilationUnit} a {@code ProtoLogCallVisitor} callback + * is executed. + */ +open class ProtoLogCallProcessor( + private val protoLogClassName: String, + private val protoLogGroupClassName: String, + private val groupMap: Map<String, LogGroup> +) { + private val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.') + private val protoLogGroupSimpleClassName = protoLogGroupClassName.substringAfterLast('.') + + private fun getLogGroupName( + expr: Expression, + isClassImported: Boolean, + staticImports: Set<String>, + fileName: String + ): String { + val context = ParsingContext(fileName, expr) + return when (expr) { + is NameExpr -> when { + expr.nameAsString in staticImports -> expr.nameAsString + else -> + throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr", + context) + } + is FieldAccessExpr -> when { + expr.scope.toString() == protoLogGroupClassName + || isClassImported && + expr.scope.toString() == protoLogGroupSimpleClassName -> expr.nameAsString + else -> + throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr", + context) + } + else -> throw InvalidProtoLogCallException("Invalid group argument " + + "- must be ProtoLogGroup enum member reference: $expr", context) + } + } + + private fun isProtoCall( + call: MethodCallExpr, + isLogClassImported: Boolean, + staticLogImports: Collection<String> + ): Boolean { + return call.scope.isPresent && call.scope.get().toString() == protoLogClassName || + isLogClassImported && call.scope.isPresent && + call.scope.get().toString() == protoLogSimpleClassName || + !call.scope.isPresent && staticLogImports.contains(call.name.toString()) + } + + open fun process(code: CompilationUnit, callVisitor: ProtoLogCallVisitor?, fileName: String): + CompilationUnit { + CodeUtils.checkWildcardStaticImported(code, protoLogClassName, fileName) + CodeUtils.checkWildcardStaticImported(code, protoLogGroupClassName, fileName) + + val isLogClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogClassName) + val staticLogImports = CodeUtils.staticallyImportedMethods(code, protoLogClassName) + val isGroupClassImported = CodeUtils.isClassImportedOrSamePackage(code, + protoLogGroupClassName) + val staticGroupImports = CodeUtils.staticallyImportedMethods(code, protoLogGroupClassName) + + code.findAll(MethodCallExpr::class.java) + .filter { call -> + isProtoCall(call, isLogClassImported, staticLogImports) + }.forEach { call -> + val context = ParsingContext(fileName, call) + if (call.arguments.size < 2) { + throw InvalidProtoLogCallException("Method signature does not match " + + "any ProtoLog method: $call", context) + } + + val messageString = CodeUtils.concatMultilineString(call.getArgument(1), + context) + val groupNameArg = call.getArgument(0) + val groupName = + getLogGroupName(groupNameArg, isGroupClassImported, + staticGroupImports, fileName) + if (groupName !in groupMap) { + throw InvalidProtoLogCallException("Unknown group argument " + + "- not a ProtoLogGroup enum member: $call", context) + } + + callVisitor?.processCall(call, messageString, LogLevel.getLevelForMethodName( + call.name.toString(), call, context), groupMap.getValue(groupName)) + } + return code + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt new file mode 100644 index 000000000000..aa58b69d61cb --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.ast.expr.MethodCallExpr + +interface ProtoLogCallVisitor { + fun processCall(call: MethodCallExpr, messageString: String, level: LogLevel, group: LogGroup) +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt new file mode 100644 index 000000000000..75493b6427cb --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.protolog.tool.Constants.ENUM_VALUES_METHOD +import com.android.server.protolog.common.IProtoLogGroup +import java.io.File +import java.net.URLClassLoader + +class ProtoLogGroupReader { + private fun getClassloaderForJar(jarPath: String): ClassLoader { + val jarFile = File(jarPath) + val url = jarFile.toURI().toURL() + return URLClassLoader(arrayOf(url), ProtoLogGroupReader::class.java.classLoader) + } + + private fun getEnumValues(clazz: Class<*>): List<IProtoLogGroup> { + val valuesMethod = clazz.getMethod(ENUM_VALUES_METHOD) + @Suppress("UNCHECKED_CAST") + return (valuesMethod.invoke(null) as Array<IProtoLogGroup>).toList() + } + + fun loadFromJar(jarPath: String, className: String): Map<String, LogGroup> { + try { + val classLoader = getClassloaderForJar(jarPath) + val clazz = classLoader.loadClass(className) + val values = getEnumValues(clazz) + return values.map { group -> + group.name() to + LogGroup(group.name(), group.isEnabled, group.isLogToLogcat, group.tag) + }.toMap() + } catch (ex: ReflectiveOperationException) { + throw RuntimeException("Unable to load ProtoLogGroup enum class", ex) + } + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt new file mode 100644 index 000000000000..3c55237ce443 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.protolog.tool.CommandOptions.Companion.USAGE +import com.github.javaparser.ParseProblemException +import com.github.javaparser.ParserConfiguration +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.OutputStream +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.jar.JarOutputStream +import java.util.zip.ZipEntry +import kotlin.system.exitProcess + +object ProtoLogTool { + private fun showHelpAndExit() { + println(USAGE) + exitProcess(-1) + } + + private fun containsProtoLogText(source: String, protoLogClassName: String): Boolean { + val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.') + return source.contains(protoLogSimpleClassName) + } + + private fun processClasses(command: CommandOptions) { + val groups = injector.readLogGroups( + command.protoLogGroupsJarArg, + command.protoLogGroupsClassNameArg) + val out = injector.fileOutputStream(command.outputSourceJarArg) + val outJar = JarOutputStream(out) + val processor = ProtoLogCallProcessor(command.protoLogClassNameArg, + command.protoLogGroupsClassNameArg, groups) + + val executor = newThreadPool() + + try { + command.javaSourceArgs.map { path -> + executor.submitCallable { + val transformer = SourceTransformer(command.protoLogImplClassNameArg, + command.protoLogCacheClassNameArg, processor) + val file = File(path) + val text = injector.readText(file) + val outSrc = try { + val code = tryParse(text, path) + if (containsProtoLogText(text, command.protoLogClassNameArg)) { + transformer.processClass(text, path, packagePath(file, code), code) + } else { + text + } + } catch (ex: ParsingException) { + // If we cannot parse this file, skip it (and log why). Compilation will + // fail in a subsequent build step. + injector.reportParseError(ex) + text + } + path to outSrc + } + }.map { future -> + val (path, outSrc) = future.get() + outJar.putNextEntry(ZipEntry(path)) + outJar.write(outSrc.toByteArray()) + outJar.closeEntry() + } + } finally { + executor.shutdown() + } + + val cacheSplit = command.protoLogCacheClassNameArg.split(".") + val cacheName = cacheSplit.last() + val cachePackage = cacheSplit.dropLast(1).joinToString(".") + val cachePath = "gen/${cacheSplit.joinToString("/")}.java" + + outJar.putNextEntry(ZipEntry(cachePath)) + outJar.write(generateLogGroupCache(cachePackage, cacheName, groups, + command.protoLogImplClassNameArg, command.protoLogGroupsClassNameArg).toByteArray()) + + outJar.close() + out.close() + } + + fun generateLogGroupCache( + cachePackage: String, + cacheName: String, + groups: Map<String, LogGroup>, + protoLogImplClassName: String, + protoLogGroupsClassName: String + ): String { + val fields = groups.values.map { + "public static boolean ${it.name}_enabled = false;" + }.joinToString("\n") + + val updates = groups.values.map { + "${it.name}_enabled = " + + "$protoLogImplClassName.isEnabled($protoLogGroupsClassName.${it.name});" + }.joinToString("\n") + + return """ + package $cachePackage; + + public class $cacheName { +${fields.replaceIndent(" ")} + + static { + $protoLogImplClassName.sCacheUpdater = $cacheName::update; + update(); + } + + static void update() { +${updates.replaceIndent(" ")} + } + } + """.trimIndent() + } + + private fun tryParse(code: String, fileName: String): CompilationUnit { + try { + return StaticJavaParser.parse(code) + } catch (ex: ParseProblemException) { + val problem = ex.problems.first() + throw ParsingException("Java parsing erro" + + "r: ${problem.verboseMessage}", + ParsingContext(fileName, problem.location.orElse(null) + ?.begin?.range?.orElse(null)?.begin?.line + ?: 0)) + } + } + + private fun viewerConf(command: CommandOptions) { + val groups = injector.readLogGroups( + command.protoLogGroupsJarArg, + command.protoLogGroupsClassNameArg) + val processor = ProtoLogCallProcessor(command.protoLogClassNameArg, + command.protoLogGroupsClassNameArg, groups) + val builder = ViewerConfigBuilder(processor) + + val executor = newThreadPool() + + try { + command.javaSourceArgs.map { path -> + executor.submitCallable { + val file = File(path) + val text = injector.readText(file) + if (containsProtoLogText(text, command.protoLogClassNameArg)) { + try { + val code = tryParse(text, path) + builder.findLogCalls(code, path, packagePath(file, code)) + } catch (ex: ParsingException) { + // If we cannot parse this file, skip it (and log why). Compilation will + // fail in a subsequent build step. + injector.reportParseError(ex) + null + } + } else { + null + } + } + }.forEach { future -> + builder.addLogCalls(future.get() ?: return@forEach) + } + } finally { + executor.shutdown() + } + + val out = injector.fileOutputStream(command.viewerConfigJsonArg) + out.write(builder.build().toByteArray()) + out.close() + } + + private fun packagePath(file: File, code: CompilationUnit): String { + val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration + .get().nameAsString else "" + val packagePath = pack.replace('.', '/') + '/' + file.name + return packagePath + } + + private fun read(command: CommandOptions) { + LogParser(ViewerConfigParser()) + .parse(FileInputStream(command.logProtofileArg), + FileInputStream(command.viewerConfigJsonArg), System.out) + } + + @JvmStatic + fun main(args: Array<String>) { + try { + val command = CommandOptions(args) + invoke(command) + } catch (ex: InvalidCommandException) { + println("\n${ex.message}\n") + showHelpAndExit() + } catch (ex: CodeProcessingException) { + println("\n${ex.message}\n") + exitProcess(1) + } + } + + fun invoke(command: CommandOptions) { + StaticJavaParser.setConfiguration(ParserConfiguration().apply { + setLanguageLevel(ParserConfiguration.LanguageLevel.RAW) + setAttributeComments(false) + }) + + when (command.command) { + CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command) + CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command) + CommandOptions.READ_LOG_CMD -> read(command) + } + } + + var injector = object : Injector { + override fun fileOutputStream(file: String) = FileOutputStream(file) + override fun readText(file: File) = file.readText() + override fun readLogGroups(jarPath: String, className: String) = + ProtoLogGroupReader().loadFromJar(jarPath, className) + override fun reportParseError(ex: ParsingException) { + println("\n${ex.message}\n") + } + } + + interface Injector { + fun fileOutputStream(file: String): OutputStream + fun readText(file: File): String + fun readLogGroups(jarPath: String, className: String): Map<String, LogGroup> + fun reportParseError(ex: ParsingException) + } +} + +private fun <T> ExecutorService.submitCallable(f: () -> T) = submit(f) + +private fun newThreadPool() = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors()) diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt new file mode 100644 index 000000000000..36ea41129450 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.server.protolog.common.LogDataType +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.NodeList +import com.github.javaparser.ast.body.VariableDeclarator +import com.github.javaparser.ast.expr.BooleanLiteralExpr +import com.github.javaparser.ast.expr.CastExpr +import com.github.javaparser.ast.expr.Expression +import com.github.javaparser.ast.expr.FieldAccessExpr +import com.github.javaparser.ast.expr.IntegerLiteralExpr +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.expr.NameExpr +import com.github.javaparser.ast.expr.NullLiteralExpr +import com.github.javaparser.ast.expr.SimpleName +import com.github.javaparser.ast.expr.TypeExpr +import com.github.javaparser.ast.expr.VariableDeclarationExpr +import com.github.javaparser.ast.stmt.BlockStmt +import com.github.javaparser.ast.stmt.ExpressionStmt +import com.github.javaparser.ast.stmt.IfStmt +import com.github.javaparser.ast.type.ArrayType +import com.github.javaparser.ast.type.ClassOrInterfaceType +import com.github.javaparser.ast.type.PrimitiveType +import com.github.javaparser.ast.type.Type +import com.github.javaparser.printer.PrettyPrinter +import com.github.javaparser.printer.PrettyPrinterConfiguration + +class SourceTransformer( + protoLogImplClassName: String, + protoLogCacheClassName: String, + private val protoLogCallProcessor: ProtoLogCallProcessor +) : ProtoLogCallVisitor { + override fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup + ) { + // Input format: ProtoLog.e(GROUP, "msg %d", arg) + if (!call.parentNode.isPresent) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- no parent node in AST") + } + if (call.parentNode.get() !is ExpressionStmt) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- parent node in AST is not an ExpressionStmt") + } + val parentStmt = call.parentNode.get() as ExpressionStmt + if (!parentStmt.parentNode.isPresent) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- no grandparent node in AST") + } + val ifStmt: IfStmt + if (group.enabled) { + val hash = CodeUtils.hash(packagePath, messageString, level, group) + val newCall = call.clone() + if (!group.textEnabled) { + // Remove message string if text logging is not enabled by default. + // Out: ProtoLog.e(GROUP, null, arg) + newCall.arguments[1].replace(NameExpr("null")) + } + // Insert message string hash as a second argument. + // Out: ProtoLog.e(GROUP, 1234, null, arg) + newCall.arguments.add(1, IntegerLiteralExpr(hash)) + val argTypes = LogDataType.parseFormatString(messageString) + val typeMask = LogDataType.logDataTypesToBitMask(argTypes) + // Insert bitmap representing which Number parameters are to be considered as + // floating point numbers. + // Out: ProtoLog.e(GROUP, 1234, 0, null, arg) + newCall.arguments.add(2, IntegerLiteralExpr(typeMask)) + // Replace call to a stub method with an actual implementation. + // Out: com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, null, arg) + newCall.setScope(protoLogImplClassNode) + // Create a call to ProtoLog$Cache.GROUP_enabled + // Out: com.android.server.protolog.ProtoLog$Cache.GROUP_enabled + val isLogEnabled = FieldAccessExpr(protoLogCacheClassNode, "${group.name}_enabled") + if (argTypes.size != call.arguments.size - 2) { + throw InvalidProtoLogCallException( + "Number of arguments (${argTypes.size} does not mach format" + + " string in: $call", ParsingContext(path, call)) + } + val blockStmt = BlockStmt() + if (argTypes.isNotEmpty()) { + // Assign every argument to a variable to check its type in compile time + // (this is assignment is optimized-out by dex tool, there is no runtime impact)/ + // Out: long protoLogParam0 = arg + argTypes.forEachIndexed { idx, type -> + val varName = "protoLogParam$idx" + val declaration = VariableDeclarator(getASTTypeForDataType(type), varName, + getConversionForType(type)(newCall.arguments[idx + 4].clone())) + blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration))) + newCall.setArgument(idx + 4, NameExpr(SimpleName(varName))) + } + } else { + // Assign (Object[])null as the vararg parameter to prevent allocating an empty + // object array. + val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr()) + newCall.addArgument(nullArray) + } + blockStmt.addStatement(ExpressionStmt(newCall)) + // Create an IF-statement with the previously created condition. + // Out: if (com.android.server.protolog.ProtoLogImpl.isEnabled(GROUP)) { + // long protoLogParam0 = arg; + // com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0); + // } + ifStmt = IfStmt(isLogEnabled, blockStmt, null) + } else { + // Surround with if (false). + val newCall = parentStmt.clone() + ifStmt = IfStmt(BooleanLiteralExpr(false), BlockStmt(NodeList(newCall)), null) + newCall.setBlockComment(" ${group.name} is disabled ") + } + // Inline the new statement. + val printedIfStmt = inlinePrinter.print(ifStmt) + // Append blank lines to preserve line numbering in file (to allow debugging) + val parentRange = parentStmt.range.get() + val newLines = parentRange.end.line - parentRange.begin.line + val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}' + // pre-workaround code, see explanation below + /* + val inlinedIfStmt = StaticJavaParser.parseStatement(newStmt) + LexicalPreservingPrinter.setup(inlinedIfStmt) + // Replace the original call. + if (!parentStmt.replace(inlinedIfStmt)) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- unable to replace the call.") + } + */ + /** Workaround for a bug in JavaParser (AST tree invalid after replacing a node when using + * LexicalPreservingPrinter (https://github.com/javaparser/javaparser/issues/2290). + * Replace the code below with the one commended-out above one the issue is resolved. */ + if (!parentStmt.range.isPresent) { + // Should never happen + throw RuntimeException("Unable to process log call $call " + + "- unable to replace the call.") + } + val range = parentStmt.range.get() + val begin = range.begin.line - 1 + val oldLines = processedCode.subList(begin, range.end.line) + val oldCode = oldLines.joinToString("\n") + val newCode = oldCode.replaceRange( + offsets[begin] + range.begin.column - 1, + oldCode.length - oldLines.lastOrNull()!!.length + + range.end.column + offsets[range.end.line - 1], newStmt) + newCode.split("\n").forEachIndexed { idx, line -> + offsets[begin + idx] += line.length - processedCode[begin + idx].length + processedCode[begin + idx] = line + } + } + + private val inlinePrinter: PrettyPrinter + private val objectType = StaticJavaParser.parseClassOrInterfaceType("Object") + + init { + val config = PrettyPrinterConfiguration() + config.endOfLineCharacter = " " + config.indentSize = 0 + config.tabWidth = 1 + inlinePrinter = PrettyPrinter(config) + } + + companion object { + private val stringType: ClassOrInterfaceType = + StaticJavaParser.parseClassOrInterfaceType("String") + + fun getASTTypeForDataType(type: Int): Type { + return when (type) { + LogDataType.STRING -> stringType.clone() + LogDataType.LONG -> PrimitiveType.longType() + LogDataType.DOUBLE -> PrimitiveType.doubleType() + LogDataType.BOOLEAN -> PrimitiveType.booleanType() + else -> { + // Should never happen. + throw RuntimeException("Invalid LogDataType") + } + } + } + + fun getConversionForType(type: Int): (Expression) -> Expression { + return when (type) { + LogDataType.STRING -> { expr -> + MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")), + SimpleName("valueOf"), NodeList(expr)) + } + else -> { expr -> expr } + } + } + } + + private val protoLogImplClassNode = + StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName) + private val protoLogCacheClassNode = + StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogCacheClassName) + private var processedCode: MutableList<String> = mutableListOf() + private var offsets: IntArray = IntArray(0) + /** The path of the file being processed, relative to $ANDROID_BUILD_TOP */ + private var path: String = "" + /** The path of the file being processed, relative to the root package */ + private var packagePath: String = "" + + fun processClass( + code: String, + path: String, + packagePath: String, + compilationUnit: CompilationUnit = + StaticJavaParser.parse(code) + ): String { + this.path = path + this.packagePath = packagePath + processedCode = code.split('\n').toMutableList() + offsets = IntArray(processedCode.size) + protoLogCallProcessor.process(compilationUnit, this, path) + return processedCode.joinToString("\n") + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt new file mode 100644 index 000000000000..175c71ff810b --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonWriter +import com.github.javaparser.ast.CompilationUnit +import com.android.protolog.tool.Constants.VERSION +import com.github.javaparser.ast.expr.MethodCallExpr +import java.io.StringWriter + +class ViewerConfigBuilder( + private val processor: ProtoLogCallProcessor +) { + private fun addLogCall(logCall: LogCall, context: ParsingContext) { + val group = logCall.logGroup + val messageString = logCall.messageString + if (group.enabled) { + val key = logCall.key() + if (statements.containsKey(key)) { + if (statements[key] != logCall) { + throw HashCollisionException( + "Please modify the log message \"$messageString\" " + + "or \"${statements[key]}\" - their hashes are equal.", context) + } + } else { + groups.add(group) + statements[key] = logCall + } + } + } + + private val statements: MutableMap<Int, LogCall> = mutableMapOf() + private val groups: MutableSet<LogGroup> = mutableSetOf() + + fun findLogCalls( + unit: CompilationUnit, + path: String, + packagePath: String + ): List<Pair<LogCall, ParsingContext>> { + val calls = mutableListOf<Pair<LogCall, ParsingContext>>() + val visitor = object : ProtoLogCallVisitor { + override fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup + ) { + val logCall = LogCall(messageString, level, group, packagePath) + val context = ParsingContext(path, call) + calls.add(logCall to context) + } + } + processor.process(unit, visitor, path) + + return calls + } + + fun addLogCalls(calls: List<Pair<LogCall, ParsingContext>>) { + calls.forEach { (logCall, context) -> + addLogCall(logCall, context) + } + } + + fun build(): String { + val stringWriter = StringWriter() + val writer = JsonWriter(stringWriter) + writer.setIndent(" ") + writer.beginObject() + writer.name("version") + writer.value(VERSION) + writer.name("messages") + writer.beginObject() + statements.toSortedMap().forEach { (key, value) -> + writer.name(key.toString()) + writer.beginObject() + writer.name("message") + writer.value(value.messageString) + writer.name("level") + writer.value(value.logLevel.name) + writer.name("group") + writer.value(value.logGroup.name) + writer.name("at") + writer.value(value.position) + writer.endObject() + } + writer.endObject() + writer.name("groups") + writer.beginObject() + groups.toSortedSet(Comparator { o1, o2 -> o1.name.compareTo(o2.name) }).forEach { group -> + writer.name(group.name) + writer.beginObject() + writer.name("tag") + writer.value(group.tag) + writer.endObject() + } + writer.endObject() + writer.endObject() + stringWriter.buffer.append('\n') + return stringWriter.toString() + } + + data class LogCall( + val messageString: String, + val logLevel: LogLevel, + val logGroup: LogGroup, + val position: String + ) { + fun key() = CodeUtils.hash(position, messageString, logLevel, logGroup) + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt new file mode 100644 index 000000000000..7278db0094e6 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader + +open class ViewerConfigParser { + data class MessageEntry( + val messageString: String, + val level: String, + val groupName: String + ) + + fun parseMessage(jsonReader: JsonReader): MessageEntry { + jsonReader.beginObject() + var message: String? = null + var level: String? = null + var groupName: String? = null + while (jsonReader.hasNext()) { + when (jsonReader.nextName()) { + "message" -> message = jsonReader.nextString() + "level" -> level = jsonReader.nextString() + "group" -> groupName = jsonReader.nextString() + else -> jsonReader.skipValue() + } + } + jsonReader.endObject() + if (message.isNullOrBlank() || level.isNullOrBlank() || groupName.isNullOrBlank()) { + throw InvalidViewerConfigException("Invalid message entry in viewer config") + } + return MessageEntry(message, level, groupName) + } + + data class GroupEntry(val tag: String) + + fun parseGroup(jsonReader: JsonReader): GroupEntry { + jsonReader.beginObject() + var tag: String? = null + while (jsonReader.hasNext()) { + when (jsonReader.nextName()) { + "tag" -> tag = jsonReader.nextString() + else -> jsonReader.skipValue() + } + } + jsonReader.endObject() + if (tag.isNullOrBlank()) { + throw InvalidViewerConfigException("Invalid group entry in viewer config") + } + return GroupEntry(tag) + } + + fun parseMessages(jsonReader: JsonReader): Map<Int, MessageEntry> { + val config: MutableMap<Int, MessageEntry> = mutableMapOf() + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val key = jsonReader.nextName() + val hash = key.toIntOrNull() + ?: throw InvalidViewerConfigException("Invalid key in messages viewer config") + config[hash] = parseMessage(jsonReader) + } + jsonReader.endObject() + return config + } + + fun parseGroups(jsonReader: JsonReader): Map<String, GroupEntry> { + val config: MutableMap<String, GroupEntry> = mutableMapOf() + jsonReader.beginObject() + while (jsonReader.hasNext()) { + val key = jsonReader.nextName() + config[key] = parseGroup(jsonReader) + } + jsonReader.endObject() + return config + } + + data class ConfigEntry(val messageString: String, val level: String, val tag: String) + + open fun parseConfig(jsonReader: JsonReader): Map<Int, ConfigEntry> { + var messages: Map<Int, MessageEntry>? = null + var groups: Map<String, GroupEntry>? = null + var version: String? = null + + jsonReader.beginObject() + while (jsonReader.hasNext()) { + when (jsonReader.nextName()) { + "messages" -> messages = parseMessages(jsonReader) + "groups" -> groups = parseGroups(jsonReader) + "version" -> version = jsonReader.nextString() + + else -> jsonReader.skipValue() + } + } + jsonReader.endObject() + if (messages == null || groups == null || version == null) { + throw InvalidViewerConfigException("Invalid config - definitions missing") + } + if (version != Constants.VERSION) { + throw InvalidViewerConfigException("Viewer config version not supported by this tool," + + " config version $version, viewer version ${Constants.VERSION}") + } + return messages.map { msg -> + msg.key to ConfigEntry( + msg.value.messageString, msg.value.level, groups[msg.value.groupName]?.tag + ?: throw InvalidViewerConfigException( + "Group definition missing for ${msg.value.groupName}")) + }.toMap() + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/exceptions.kt b/tools/protologtool/src/com/android/protolog/tool/exceptions.kt new file mode 100644 index 000000000000..ae00df123353 --- /dev/null +++ b/tools/protologtool/src/com/android/protolog/tool/exceptions.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import java.lang.Exception + +open class CodeProcessingException(message: String, context: ParsingContext) + : Exception("Code processing error in ${context.filePath}:${context.lineNumber}:\n" + + " $message") + +class HashCollisionException(message: String, context: ParsingContext) : + CodeProcessingException(message, context) + +class IllegalImportException(message: String, context: ParsingContext) : + CodeProcessingException("Illegal import: $message", context) + +class InvalidProtoLogCallException(message: String, context: ParsingContext) + : CodeProcessingException("InvalidProtoLogCall: $message", context) + +class ParsingException(message: String, context: ParsingContext) + : CodeProcessingException(message, context) + +class InvalidViewerConfigException(message: String) : Exception(message) + +class InvalidInputException(message: String) : Exception(message) + +class InvalidCommandException(message: String) : Exception(message) diff --git a/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt new file mode 100644 index 000000000000..b916f8f00a68 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.expr.BinaryExpr +import com.github.javaparser.ast.expr.StringLiteralExpr +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class CodeUtilsTest { + @Test + fun hash() { + assertEquals(-1259556708, CodeUtils.hash("Test.java:50", "test", + LogLevel.DEBUG, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeLocation() { + assertEquals(15793504, CodeUtils.hash("Test.java:10", "test2", + LogLevel.DEBUG, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeLevel() { + assertEquals(-731772463, CodeUtils.hash("Test.java:50", "test", + LogLevel.ERROR, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeMessage() { + assertEquals(-2026343204, CodeUtils.hash("Test.java:50", "test2", + LogLevel.DEBUG, LogGroup("test", true, true, "TAG"))) + } + + @Test + fun hash_changeGroup() { + assertEquals(1607870166, CodeUtils.hash("Test.java:50", "test2", + LogLevel.DEBUG, LogGroup("test2", true, true, "TAG"))) + } + + @Test(expected = IllegalImportException::class) + fun checkWildcardStaticImported_true() { + val code = """package org.example.test; + import static org.example.Test.*; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun checkWildcardStaticImported_notStatic() { + val code = """package org.example.test; + import org.example.Test.*; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun checkWildcardStaticImported_differentClass() { + val code = """package org.example.test; + import static org.example.Test2.*; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun checkWildcardStaticImported_notWildcard() { + val code = """package org.example.test; + import org.example.Test.test; + """ + CodeUtils.checkWildcardStaticImported( + StaticJavaParser.parse(code), "org.example.Test", "") + } + + @Test + fun isClassImportedOrSamePackage_imported() { + val code = """package org.example.test; + import org.example.Test; + """ + assertTrue(CodeUtils.isClassImportedOrSamePackage( + StaticJavaParser.parse(code), "org.example.Test")) + } + + @Test + fun isClassImportedOrSamePackage_samePackage() { + val code = """package org.example.test; + """ + assertTrue(CodeUtils.isClassImportedOrSamePackage( + StaticJavaParser.parse(code), "org.example.test.Test")) + } + + @Test + fun isClassImportedOrSamePackage_false() { + val code = """package org.example.test; + import org.example.Test; + """ + assertFalse(CodeUtils.isClassImportedOrSamePackage( + StaticJavaParser.parse(code), "org.example.Test2")) + } + + @Test + fun staticallyImportedMethods_ab() { + val code = """ + import static org.example.Test.a; + import static org.example.Test.b; + """ + val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code), + "org.example.Test") + assertTrue(imported.containsAll(listOf("a", "b"))) + assertEquals(2, imported.size) + } + + @Test + fun staticallyImportedMethods_differentClass() { + val code = """ + import static org.example.Test.a; + import static org.example.Test2.b; + """ + val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code), + "org.example.Test") + assertTrue(imported.containsAll(listOf("a"))) + assertEquals(1, imported.size) + } + + @Test + fun staticallyImportedMethods_notStatic() { + val code = """ + import static org.example.Test.a; + import org.example.Test.b; + """ + val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code), + "org.example.Test") + assertTrue(imported.containsAll(listOf("a"))) + assertEquals(1, imported.size) + } + + @Test + fun concatMultilineString_single() { + val str = StringLiteralExpr("test") + val out = CodeUtils.concatMultilineString(str, ParsingContext()) + assertEquals("test", out) + } + + @Test + fun concatMultilineString_double() { + val str = """ + "test" + "abc" + """ + val code = StaticJavaParser.parseExpression<BinaryExpr>(str) + val out = CodeUtils.concatMultilineString(code, ParsingContext()) + assertEquals("testabc", out) + } + + @Test + fun concatMultilineString_multiple() { + val str = """ + "test" + "abc" + "1234" + "test" + """ + val code = StaticJavaParser.parseExpression<BinaryExpr>(str) + val out = CodeUtils.concatMultilineString(code, ParsingContext()) + assertEquals("testabc1234test", out) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt new file mode 100644 index 000000000000..cf36651c3e39 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import org.junit.Assert.assertEquals +import org.junit.Test + +class CommandOptionsTest { + companion object { + val TEST_JAVA_SRC = listOf( + "frameworks/base/services/core/java/com/android/server/wm/" + + "AccessibilityController.java", + "frameworks/base/services/core/java/com/android/server/wm/ActivityDisplay.java", + "frameworks/base/services/core/java/com/android/server/wm/" + + "ActivityMetricsLaunchObserver.java" + ) + private const val TEST_PROTOLOG_CLASS = "com.android.server.wm.ProtoLog" + private const val TEST_PROTOLOGIMPL_CLASS = "com.android.server.wm.ProtoLogImpl" + private const val TEST_PROTOLOGCACHE_CLASS = "com.android.server.wm.ProtoLog\$Cache" + private const val TEST_PROTOLOGGROUP_CLASS = "com.android.server.wm.ProtoLogGroup" + private const val TEST_PROTOLOGGROUP_JAR = "out/soong/.intermediates/frameworks/base/" + + "services/core/services.core.wm.protologgroups/android_common/javac/" + + "services.core.wm.protologgroups.jar" + private const val TEST_SRC_JAR = "out/soong/.temp/sbox175955373/" + + "services.core.wm.protolog.srcjar" + private const val TEST_VIEWER_JSON = "out/soong/.temp/sbox175955373/" + + "services.core.wm.protolog.json" + private const val TEST_LOG = "./test_log.pb" + } + + @Test(expected = InvalidCommandException::class) + fun noCommand() { + CommandOptions(arrayOf()) + } + + @Test(expected = InvalidCommandException::class) + fun invalidCommand() { + val testLine = "invalid" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test + fun transformClasses() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + val cmd = CommandOptions(testLine.split(' ').toTypedArray()) + assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command) + assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg) + assertEquals(TEST_PROTOLOGIMPL_CLASS, cmd.protoLogImplClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg) + assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg) + assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogClass() { + val testLine = "transform-protolog-calls " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogImplClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogCacheClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogGroupClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noProtoLogGroupJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noOutJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + TEST_JAVA_SRC.joinToString(" ") + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noJavaInput() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogClass() { + val testLine = "transform-protolog-calls --protolog-class invalid " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogImplClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class invalid " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogCacheClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class invalid " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogGroupClass() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class invalid " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidProtoLogGroupJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar invalid.txt " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidOutJar() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar invalid.db ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_invalidJavaInput() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR invalid.py" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_unknownParam() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--unknown test --protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun transformClasses_noValue() { + val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " + + "--protolog-impl-class " + + "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test + fun generateConfig() { + val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--viewer-conf $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}" + val cmd = CommandOptions(testLine.split(' ').toTypedArray()) + assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command) + assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg) + assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg) + assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg) + assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs) + } + + @Test(expected = InvalidCommandException::class) + fun generateConfig_noViewerConfig() { + val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + TEST_JAVA_SRC.joinToString(" ") + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test(expected = InvalidCommandException::class) + fun generateConfig_invalidViewerConfig() { + val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " + + "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " + + "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " + + "--viewer-conf invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}" + CommandOptions(testLine.split(' ').toTypedArray()) + } + + @Test + fun readLog() { + val testLine = "read-log --viewer-conf $TEST_VIEWER_JSON $TEST_LOG" + val cmd = CommandOptions(testLine.split(' ').toTypedArray()) + assertEquals(CommandOptions.READ_LOG_CMD, cmd.command) + assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg) + assertEquals(TEST_LOG, cmd.logProtofileArg) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt new file mode 100644 index 000000000000..dd8a0b1c50b4 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileNotFoundException +import java.io.OutputStream +import java.util.jar.JarInputStream + +class EndToEndTest { + + @Test + fun e2e_transform() { + val output = run( + src = "frameworks/base/org/example/Example.java" to """ + package org.example; + import com.android.server.protolog.common.ProtoLog; + import static com.android.server.wm.ProtoLogGroup.GROUP; + + class Example { + void method() { + String argString = "hello"; + int argInt = 123; + ProtoLog.d(GROUP, "Example: %s %d", argString, argInt); + } + } + """.trimIndent(), + logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"), + commandOptions = CommandOptions(arrayOf("transform-protolog-calls", + "--protolog-class", "com.android.server.protolog.common.ProtoLog", + "--protolog-impl-class", "com.android.server.protolog.ProtoLogImpl", + "--protolog-cache-class", + "com.android.server.protolog.ProtoLog${"\$\$"}Cache", + "--loggroups-class", "com.android.server.wm.ProtoLogGroup", + "--loggroups-jar", "not_required.jar", + "--output-srcjar", "out.srcjar", + "frameworks/base/org/example/Example.java")) + ) + val outSrcJar = assertLoadSrcJar(output, "out.srcjar") + assertTrue(" 2066303299," in outSrcJar["frameworks/base/org/example/Example.java"]!!) + } + + @Test + fun e2e_viewerConfig() { + val output = run( + src = "frameworks/base/org/example/Example.java" to """ + package org.example; + import com.android.server.protolog.common.ProtoLog; + import static com.android.server.wm.ProtoLogGroup.GROUP; + + class Example { + void method() { + String argString = "hello"; + int argInt = 123; + ProtoLog.d(GROUP, "Example: %s %d", argString, argInt); + } + } + """.trimIndent(), + logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"), + commandOptions = CommandOptions(arrayOf("generate-viewer-config", + "--protolog-class", "com.android.server.protolog.common.ProtoLog", + "--loggroups-class", "com.android.server.wm.ProtoLogGroup", + "--loggroups-jar", "not_required.jar", + "--viewer-conf", "out.json", + "frameworks/base/org/example/Example.java")) + ) + val viewerConfigJson = assertLoadText(output, "out.json") + assertTrue("\"2066303299\"" in viewerConfigJson) + } + + private fun assertLoadSrcJar( + outputs: Map<String, ByteArray>, + path: String + ): Map<String, String> { + val out = outputs[path] ?: fail("$path not in outputs (${outputs.keys})") + + val sources = mutableMapOf<String, String>() + JarInputStream(ByteArrayInputStream(out)).use { jarStream -> + var entry = jarStream.nextJarEntry + while (entry != null) { + if (entry.name.endsWith(".java")) { + sources[entry.name] = jarStream.reader().readText() + } + entry = jarStream.nextJarEntry + } + } + return sources + } + + private fun assertLoadText(outputs: Map<String, ByteArray>, path: String): String { + val out = outputs[path] ?: fail("$path not in outputs (${outputs.keys})") + return out.toString(Charsets.UTF_8) + } + + fun run( + src: Pair<String, String>, + logGroup: LogGroup, + commandOptions: CommandOptions + ): Map<String, ByteArray> { + val outputs = mutableMapOf<String, ByteArrayOutputStream>() + + ProtoLogTool.injector = object : ProtoLogTool.Injector { + override fun fileOutputStream(file: String): OutputStream = + ByteArrayOutputStream().also { outputs[file] = it } + + override fun readText(file: File): String { + if (file.path == src.first) { + return src.second + } + throw FileNotFoundException("expected: ${src.first}, but was $file") + } + + override fun readLogGroups(jarPath: String, className: String) = mapOf( + logGroup.name to logGroup) + + override fun reportParseError(ex: ParsingException) = throw AssertionError(ex) + } + + ProtoLogTool.invoke(commandOptions) + + return outputs.mapValues { it.value.toByteArray() } + } + + fun fail(message: String): Nothing = Assert.fail(message) as Nothing +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt new file mode 100644 index 000000000000..04a3bfa499d8 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import com.android.server.protolog.ProtoLogMessage +import com.android.server.protolog.ProtoLogFileProto +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.mock +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.io.PrintStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class LogParserTest { + private val configParser: ViewerConfigParser = mock(ViewerConfigParser::class.java) + private val parser = LogParser(configParser) + private var config: MutableMap<Int, ViewerConfigParser.ConfigEntry> = mutableMapOf() + private var outStream: OutputStream = ByteArrayOutputStream() + private var printStream: PrintStream = PrintStream(outStream) + private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) + + @Before + fun init() { + Mockito.`when`(configParser.parseConfig(any(JsonReader::class.java))).thenReturn(config) + } + + private fun <T> any(type: Class<T>): T = Mockito.any<T>(type) + + private fun getConfigDummyStream(): InputStream { + return "".byteInputStream() + } + + private fun buildProtoInput(logBuilder: ProtoLogFileProto.Builder): InputStream { + logBuilder.setVersion(Constants.VERSION) + logBuilder.magicNumber = + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + return logBuilder.build().toByteArray().inputStream() + } + + private fun testDate(timeMS: Long): String { + return dateFormat.format(Date(timeMS)) + } + + @Test + fun parse() { + config[70933285] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b", + "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(70933285) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: true\n", + outStream.toString()) + } + + @Test + fun parse_formatting() { + config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" + + " %x %e %g %s %f", "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(123) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + .addAllSint64Params(listOf(1000, 20000, 300000)) + .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1)) + .addStrParams("test") + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: " + + "true 1000 % 47040 493e0 1.000000e-01 1.00000e-05 test 1000.100000\n", + outStream.toString()) + } + + @Test + fun parse_invalidParamsTooMany() { + config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o", + "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(123) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + .addAllSint64Params(listOf(1000, 20000, 300000)) + .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1)) + .addStrParams("test") + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} INVALID: 123 - [test] [1000, 20000, 300000] " + + "[0.1, 1.0E-5, 1000.1] [true]\n", outStream.toString()) + } + + @Test + fun parse_invalidParamsNotEnough() { + config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" + + " %x %e %g %s %f", "ERROR", "WindowManager") + + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(123) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + .addStrParams("test") + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} INVALID: 123 - [test] [] [] [true]\n", + outStream.toString()) + } + + @Test(expected = InvalidInputException::class) + fun parse_invalidMagicNumber() { + val logBuilder = ProtoLogFileProto.newBuilder() + logBuilder.setVersion(Constants.VERSION) + logBuilder.magicNumber = 0 + val stream = logBuilder.build().toByteArray().inputStream() + + parser.parse(stream, getConfigDummyStream(), printStream) + } + + @Test(expected = InvalidInputException::class) + fun parse_invalidVersion() { + val logBuilder = ProtoLogFileProto.newBuilder() + logBuilder.setVersion("invalid") + logBuilder.magicNumber = + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + val stream = logBuilder.build().toByteArray().inputStream() + + parser.parse(stream, getConfigDummyStream(), printStream) + } + + @Test + fun parse_noConfig() { + val logBuilder = ProtoLogFileProto.newBuilder() + val logMessageBuilder = ProtoLogMessage.newBuilder() + logMessageBuilder + .setMessageHash(70933285) + .setElapsedRealtimeNanos(0) + .addBooleanParams(true) + logBuilder.addLog(logMessageBuilder.build()) + + parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream) + + assertEquals("${testDate(0)} UNKNOWN: 70933285 - [] [] [] [true]\n", + outStream.toString()) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt new file mode 100644 index 000000000000..97f67a0a3fdb --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.expr.MethodCallExpr +import org.junit.Assert.assertEquals +import org.junit.Test + +class ProtoLogCallProcessorTest { + private data class LogCall( + val call: MethodCallExpr, + val messageString: String, + val level: LogLevel, + val group: LogGroup + ) + + private val groupMap: MutableMap<String, LogGroup> = mutableMapOf() + private val calls: MutableList<LogCall> = mutableListOf() + private val visitor = ProtoLogCallProcessor("org.example.ProtoLog", "org.example.ProtoLogGroup", + groupMap) + private val processor = object : ProtoLogCallVisitor { + override fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup + ) { + calls.add(LogCall(call, messageString, level, group)) + } + } + + private fun checkCalls() { + assertEquals(1, calls.size) + val c = calls[0] + assertEquals("test %b", c.messageString) + assertEquals(groupMap["TEST"], c.group) + assertEquals(LogLevel.DEBUG, c.level) + } + + @Test + fun process_samePackage() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + ProtoLog.e(ProtoLogGroup.ERROR, "error %d", 1); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, false, "WindowManager") + groupMap["ERROR"] = LogGroup("ERROR", true, true, "WindowManagerERROR") + visitor.process(StaticJavaParser.parse(code), processor, "") + assertEquals(2, calls.size) + var c = calls[0] + assertEquals("test %b", c.messageString) + assertEquals(groupMap["TEST"], c.group) + assertEquals(LogLevel.DEBUG, c.level) + c = calls[1] + assertEquals("error %d", c.messageString) + assertEquals(groupMap["ERROR"], c.group) + assertEquals(LogLevel.ERROR, c.level) + } + + @Test + fun process_imported() { + val code = """ + package org.example2; + + import org.example.ProtoLog; + import org.example.ProtoLogGroup; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + checkCalls() + } + + @Test + fun process_importedStatic() { + val code = """ + package org.example2; + + import static org.example.ProtoLog.d; + import static org.example.ProtoLogGroup.TEST; + + class Test { + void test() { + d(TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + checkCalls() + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_groupNotImported() { + val code = """ + package org.example2; + + import org.example.ProtoLog; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test + fun process_protoLogNotImported() { + val code = """ + package org.example2; + + import org.example.ProtoLogGroup; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + assertEquals(0, calls.size) + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_unknownGroup() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_staticGroup() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(TEST, "test %b", true); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_badGroup() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(0, "test %b", true); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test(expected = InvalidProtoLogCallException::class) + fun process_invalidSignature() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d("test"); + } + } + """ + visitor.process(StaticJavaParser.parse(code), processor, "") + } + + @Test + fun process_disabled() { + // Disabled groups are also processed. + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "test %b", true); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", false, true, "WindowManager") + visitor.process(StaticJavaParser.parse(code), processor, "") + checkCalls() + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt new file mode 100644 index 000000000000..ea9a58d859af --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ProtoLogToolTest { + + @Test + fun generateLogGroupCache() { + val groups = mapOf( + "GROUP1" to LogGroup("GROUP1", true, true, "TAG1"), + "GROUP2" to LogGroup("GROUP2", true, true, "TAG2") + ) + val code = ProtoLogTool.generateLogGroupCache("org.example", "ProtoLog\$Cache", + groups, "org.example.ProtoLogImpl", "org.example.ProtoLogGroups") + + assertEquals(""" + package org.example; + + public class ProtoLog${'$'}Cache { + public static boolean GROUP1_enabled = false; + public static boolean GROUP2_enabled = false; + + static { + org.example.ProtoLogImpl.sCacheUpdater = ProtoLog${'$'}Cache::update; + update(); + } + + static void update() { + GROUP1_enabled = org.example.ProtoLogImpl.isEnabled(org.example.ProtoLogGroups.GROUP1); + GROUP2_enabled = org.example.ProtoLogImpl.isEnabled(org.example.ProtoLogGroups.GROUP2); + } + } + """.trimIndent(), code) + } +}
\ No newline at end of file diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt new file mode 100644 index 000000000000..4f2be328fc8a --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.stmt.IfStmt +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Test +import org.mockito.Mockito + +class SourceTransformerTest { + companion object { + private const val PROTO_LOG_IMPL_PATH = "org.example.ProtoLogImpl" + + /* ktlint-disable max-line-length */ + private val TEST_CODE = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); + } + } + """.trimIndent() + + private val TEST_CODE_MULTILINE = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test %d %f " + + "abc %s\n test", 100, + 0.1, "test"); + } + } + """.trimIndent() + + private val TEST_CODE_MULTICALLS = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); + ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); + } + } + """.trimIndent() + + private val TEST_CODE_NO_PARAMS = """ + package org.example; + + class Test { + void test() { + ProtoLog.w(TEST_GROUP, "test"); + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); + + } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_NO_PARAMS = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogCache.TEST_GROUP_enabled) { org.example.ProtoLogImpl.w(TEST_GROUP, -1741986185, 0, "test", (Object[]) null); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_TEXT_DISABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, null, protoLogParam0, protoLogParam1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED = """ + package org.example; + + class Test { + void test() { + if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); + + } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_DISABLED = """ + package org.example; + + class Test { + void test() { + if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); } + } + } + """.trimIndent() + + private val TRANSFORMED_CODE_MULTILINE_DISABLED = """ + package org.example; + + class Test { + void test() { + if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f " + "abc %s\n test", 100, 0.1, "test"); + + } + } + } + """.trimIndent() + /* ktlint-enable max-line-length */ + + private const val PATH = "com.example.Test.java" + } + + private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java) + private val implName = "org.example.ProtoLogImpl" + private val cacheName = "org.example.ProtoLogCache" + private val sourceJarWriter = SourceTransformer(implName, cacheName, processor) + + private fun <T> any(type: Class<T>): T = Mockito.any<T>(type) + + @Test + fun processClass_textEnabled() { + var code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(3, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(6, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1698911065", methodCall.arguments[1].toString()) + assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) + assertEquals("\"test %d %f\"", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals(TRANSFORMED_CODE_TEXT_ENABLED, out) + } + + @Test + fun processClass_textEnabledMulticalls() { + var code = StaticJavaParser.parse(TEST_CODE_MULTICALLS) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + val calls = code.findAll(MethodCallExpr::class.java) + visitor.processCall(calls[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + visitor.processCall(calls[1], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + visitor.processCall(calls[2], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTICALLS, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(3, ifStmts.size) + val ifStmt = ifStmts[1] + assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(3, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(6, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1698911065", methodCall.arguments[1].toString()) + assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) + assertEquals("\"test %d %f\"", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED, out) + } + + @Test + fun processClass_textEnabledMultiline() { + var code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], + "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", + true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(4, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(7, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1780316587", methodCall.arguments[1].toString()) + assertEquals(0b001001.toString(), methodCall.arguments[2].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals("protoLogParam2", methodCall.arguments[6].toString()) + assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED, out) + } + + @Test + fun processClass_noParams() { + var code = StaticJavaParser.parse(TEST_CODE_NO_PARAMS) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test", + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_NO_PARAMS, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(1, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(5, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("-1741986185", methodCall.arguments[1].toString()) + assertEquals(0.toString(), methodCall.arguments[2].toString()) + assertEquals(TRANSFORMED_CODE_NO_PARAMS, out) + } + + @Test + fun processClass_textDisabled() { + var code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", true, false, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(3, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(6, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1698911065", methodCall.arguments[1].toString()) + assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) + assertEquals("null", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals(TRANSFORMED_CODE_TEXT_DISABLED, out) + } + + @Test + fun processClass_textDisabledMultiline() { + var code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], + "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", + true, false, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString()) + assertFalse(ifStmt.elseStmt.isPresent) + assertEquals(4, ifStmt.thenStmt.childNodes.size) + val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr + assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString()) + assertEquals("w", methodCall.name.asString()) + assertEquals(7, methodCall.arguments.size) + assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) + assertEquals("1780316587", methodCall.arguments[1].toString()) + assertEquals(0b001001.toString(), methodCall.arguments[2].toString()) + assertEquals("null", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals("protoLogParam2", methodCall.arguments[6].toString()) + assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out) + } + + @Test + fun processClass_disabled() { + var code = StaticJavaParser.parse(TEST_CODE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f", + LogLevel.WARN, LogGroup("TEST_GROUP", false, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("false", ifStmt.condition.toString()) + assertEquals(TRANSFORMED_CODE_DISABLED, out) + } + + @Test + fun processClass_disabledMultiline() { + var code = StaticJavaParser.parse(TEST_CODE_MULTILINE) + + Mockito.`when`(processor.process(any(CompilationUnit::class.java), + any(ProtoLogCallVisitor::class.java), any(String::class.java))) + .thenAnswer { invocation -> + val visitor = invocation.arguments[1] as ProtoLogCallVisitor + + visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], + "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", + false, true, "WM_TEST")) + + invocation.arguments[0] as CompilationUnit + } + + val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code) + code = StaticJavaParser.parse(out) + + val ifStmts = code.findAll(IfStmt::class.java) + assertEquals(1, ifStmts.size) + val ifStmt = ifStmts[0] + assertEquals("false", ifStmt.condition.toString()) + assertEquals(TRANSFORMED_CODE_MULTILINE_DISABLED, out) + } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt new file mode 100644 index 000000000000..a24761aed9db --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import com.android.protolog.tool.ViewerConfigBuilder.LogCall +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.Mockito +import java.io.StringReader + +class ViewerConfigBuilderTest { + companion object { + private val TAG1 = "WM_TEST" + private val TAG2 = "WM_DEBUG" + private val TEST1 = ViewerConfigParser.ConfigEntry("test1", LogLevel.INFO.name, TAG1) + private val TEST2 = ViewerConfigParser.ConfigEntry("test2", LogLevel.DEBUG.name, TAG2) + private val TEST3 = ViewerConfigParser.ConfigEntry("test3", LogLevel.ERROR.name, TAG2) + private val GROUP1 = LogGroup("TEST_GROUP", true, true, TAG1) + private val GROUP2 = LogGroup("DEBUG_GROUP", true, true, TAG2) + private val GROUP3 = LogGroup("DEBUG_GROUP", true, true, TAG2) + private val GROUP_DISABLED = LogGroup("DEBUG_GROUP", false, true, TAG2) + private val GROUP_TEXT_DISABLED = LogGroup("DEBUG_GROUP", true, false, TAG2) + private const val PATH = "/tmp/test.java" + } + + private val configBuilder = ViewerConfigBuilder(Mockito.mock(ProtoLogCallProcessor::class.java)) + + private fun parseConfig(json: String): Map<Int, ViewerConfigParser.ConfigEntry> { + return ViewerConfigParser().parseConfig(JsonReader(StringReader(json))) + } + + @Test + fun processClass() { + configBuilder.addLogCalls(listOf( + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), + LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP2, PATH), + LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH)).withContext()) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(3, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, + TEST1.messageString, LogLevel.INFO, GROUP1)]) + assertEquals(TEST2, parsedConfig[CodeUtils.hash(PATH, TEST2.messageString, + LogLevel.DEBUG, GROUP2)]) + assertEquals(TEST3, parsedConfig[CodeUtils.hash(PATH, TEST3.messageString, + LogLevel.ERROR, GROUP3)]) + } + + @Test + fun processClass_nonUnique() { + configBuilder.addLogCalls(listOf( + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH)).withContext()) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(1, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString, + LogLevel.INFO, GROUP1)]) + } + + @Test + fun processClass_disabled() { + configBuilder.addLogCalls(listOf( + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), + LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP_DISABLED, PATH), + LogCall(TEST3.messageString, LogLevel.ERROR, GROUP_TEXT_DISABLED, PATH)) + .withContext()) + + val parsedConfig = parseConfig(configBuilder.build()) + assertEquals(2, parsedConfig.size) + assertEquals(TEST1, parsedConfig[CodeUtils.hash( + PATH, TEST1.messageString, LogLevel.INFO, GROUP1)]) + assertEquals(TEST3, parsedConfig[CodeUtils.hash( + PATH, TEST3.messageString, LogLevel.ERROR, GROUP_TEXT_DISABLED)]) + } + + private fun List<LogCall>.withContext() = map { it to ParsingContext() } +} diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt new file mode 100644 index 000000000000..dc3ef7c57b35 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.json.stream.JsonReader +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.StringReader + +class ViewerConfigParserTest { + private val parser = ViewerConfigParser() + + private fun getJSONReader(str: String): JsonReader { + return JsonReader(StringReader(str)) + } + + @Test + fun parseMessage() { + val json = """ + { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + """ + val msg = parser.parseMessage(getJSONReader(json)) + assertEquals("Test completed successfully: %b", msg.messageString) + assertEquals("ERROR", msg.level) + assertEquals("GENERIC_WM", msg.groupName) + } + + @Test + fun parseMessage_reorder() { + val json = """ + { + "group": "GENERIC_WM", + "level": "ERROR", + "message": "Test completed successfully: %b" + } + """ + val msg = parser.parseMessage(getJSONReader(json)) + assertEquals("Test completed successfully: %b", msg.messageString) + assertEquals("ERROR", msg.level) + assertEquals("GENERIC_WM", msg.groupName) + } + + @Test + fun parseMessage_unknownEntry() { + val json = """ + { + "unknown": "unknown entries should not block parsing", + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + """ + val msg = parser.parseMessage(getJSONReader(json)) + assertEquals("Test completed successfully: %b", msg.messageString) + assertEquals("ERROR", msg.level) + assertEquals("GENERIC_WM", msg.groupName) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessage_noMessage() { + val json = """ + { + "level": "ERROR", + "group": "GENERIC_WM" + } + """ + parser.parseMessage(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessage_noLevel() { + val json = """ + { + "message": "Test completed successfully: %b", + "group": "GENERIC_WM" + } + """ + parser.parseMessage(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessage_noGroup() { + val json = """ + { + "message": "Test completed successfully: %b", + "level": "ERROR" + } + """ + parser.parseMessage(getJSONReader(json)) + } + + @Test + fun parseGroup() { + val json = """ + { + "tag": "WindowManager" + } + """ + val group = parser.parseGroup(getJSONReader(json)) + assertEquals("WindowManager", group.tag) + } + + @Test + fun parseGroup_unknownEntry() { + val json = """ + { + "unknown": "unknown entries should not block parsing", + "tag": "WindowManager" + } + """ + val group = parser.parseGroup(getJSONReader(json)) + assertEquals("WindowManager", group.tag) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseGroup_noTag() { + val json = """ + { + } + """ + parser.parseGroup(getJSONReader(json)) + } + + @Test + fun parseMessages() { + val json = """ + { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + }, + "1792430067": { + "message": "Attempted to add window to a display that does not exist: %d. Aborting.", + "level": "WARN", + "group": "ERROR_WM" + } + } + """ + val messages = parser.parseMessages(getJSONReader(json)) + assertEquals(2, messages.size) + val msg1 = + ViewerConfigParser.MessageEntry("Test completed successfully: %b", + "ERROR", "GENERIC_WM") + val msg2 = + ViewerConfigParser.MessageEntry("Attempted to add window to a display that " + + "does not exist: %d. Aborting.", "WARN", "ERROR_WM") + + assertEquals(msg1, messages[70933285]) + assertEquals(msg2, messages[1792430067]) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseMessages_invalidHash() { + val json = """ + { + "invalid": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + } + """ + parser.parseMessages(getJSONReader(json)) + } + + @Test + fun parseGroups() { + val json = """ + { + "GENERIC_WM": { + "tag": "WindowManager" + }, + "ERROR_WM": { + "tag": "WindowManagerError" + } + } + """ + val groups = parser.parseGroups(getJSONReader(json)) + assertEquals(2, groups.size) + val grp1 = ViewerConfigParser.GroupEntry("WindowManager") + val grp2 = ViewerConfigParser.GroupEntry("WindowManagerError") + assertEquals(grp1, groups["GENERIC_WM"]) + assertEquals(grp2, groups["ERROR_WM"]) + } + + @Test + fun parseConfig() { + val json = """ + { + "version": "${Constants.VERSION}", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + val config = parser.parseConfig(getJSONReader(json)) + assertEquals(1, config.size) + val cfg1 = ViewerConfigParser.ConfigEntry("Test completed successfully: %b", + "ERROR", "WindowManager") + assertEquals(cfg1, config[70933285]) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_invalidVersion() { + val json = """ + { + "version": "invalid", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_noVersion() { + val json = """ + { + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_noMessages() { + val json = """ + { + "version": "${Constants.VERSION}", + "groups": { + "GENERIC_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_noGroups() { + val json = """ + { + "version": "${Constants.VERSION}", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } + + @Test(expected = InvalidViewerConfigException::class) + fun parseConfig_missingGroup() { + val json = """ + { + "version": "${Constants.VERSION}", + "messages": { + "70933285": { + "message": "Test completed successfully: %b", + "level": "ERROR", + "group": "GENERIC_WM" + } + }, + "groups": { + "ERROR_WM": { + "tag": "WindowManager" + } + } + } + """ + parser.parseConfig(getJSONReader(json)) + } +} diff --git a/tools/stats_log_api_gen/.clang-format b/tools/stats_log_api_gen/.clang-format new file mode 100644 index 000000000000..cead3a079435 --- /dev/null +++ b/tools/stats_log_api_gen/.clang-format @@ -0,0 +1,17 @@ +BasedOnStyle: Google +AllowShortIfStatementsOnASingleLine: true +AllowShortFunctionsOnASingleLine: false +AllowShortLoopsOnASingleLine: true +BinPackArguments: true +BinPackParameters: true +ColumnLimit: 100 +CommentPragmas: NOLINT:.* +ContinuationIndentWidth: 8 +DerivePointerAlignment: false +IndentWidth: 4 +PointerAlignment: Left +TabWidth: 4 +AccessModifierOffset: -4 +IncludeCategories: + - Regex: '^"Log\.h"' + Priority: -1 diff --git a/tools/stats_log_api_gen/Android.bp b/tools/stats_log_api_gen/Android.bp index d3958a65c704..e3b6db08c503 100644 --- a/tools/stats_log_api_gen/Android.bp +++ b/tools/stats_log_api_gen/Android.bp @@ -21,16 +21,13 @@ cc_binary_host { name: "stats-log-api-gen", srcs: [ "Collation.cpp", - "atoms_info_writer.cpp", "java_writer.cpp", "java_writer_q.cpp", "main.cpp", "native_writer.cpp", - "native_writer_q.cpp", "utils.cpp", ], cflags: [ - "-DSTATS_SCHEMA_LEGACY", "-Wall", "-Werror", ], @@ -122,6 +119,13 @@ cc_library { "liblog", "libcutils", ], - static_libs: ["libstatssocket"], + target: { + android: { + shared_libs: ["libstatssocket"], + }, + host: { + static_libs: ["libstatssocket"], + }, + }, } diff --git a/tools/stats_log_api_gen/Collation.cpp b/tools/stats_log_api_gen/Collation.cpp index 75deb017e41b..a230de46dcf3 100644 --- a/tools/stats_log_api_gen/Collation.cpp +++ b/tools/stats_log_api_gen/Collation.cpp @@ -15,11 +15,13 @@ */ #include "Collation.h" -#include "frameworks/base/cmds/statsd/src/atoms.pb.h" #include <stdio.h> + #include <map> +#include "frameworks/base/cmds/statsd/src/atoms.pb.h" + namespace android { namespace stats_log_api_gen { @@ -27,17 +29,16 @@ using google::protobuf::EnumDescriptor; using google::protobuf::FieldDescriptor; using google::protobuf::FileDescriptor; using google::protobuf::SourceLocation; +using std::make_shared; using std::map; +const bool dbg = false; // // AtomDecl class // -AtomDecl::AtomDecl() - :code(0), - name() -{ +AtomDecl::AtomDecl() : code(0), name() { } AtomDecl::AtomDecl(const AtomDecl& that) @@ -45,38 +46,33 @@ AtomDecl::AtomDecl(const AtomDecl& that) name(that.name), message(that.message), fields(that.fields), + fieldNumberToAnnotations(that.fieldNumberToAnnotations), primaryFields(that.primaryFields), exclusiveField(that.exclusiveField), - uidField(that.uidField), - whitelisted(that.whitelisted), - binaryFields(that.binaryFields), - hasModule(that.hasModule), - moduleName(that.moduleName) {} - -AtomDecl::AtomDecl(int c, const string& n, const string& m) - :code(c), - name(n), - message(m) -{ + defaultState(that.defaultState), + triggerStateReset(that.triggerStateReset), + nested(that.nested), + uidField(that.uidField) { } -AtomDecl::~AtomDecl() -{ +AtomDecl::AtomDecl(int c, const string& n, const string& m) : code(c), name(n), message(m) { } +AtomDecl::~AtomDecl() { +} /** - * Print an error message for a FieldDescriptor, including the file name and line number. + * Print an error message for a FieldDescriptor, including the file name and + * line number. */ -static void -print_error(const FieldDescriptor* field, const char* format, ...) -{ +static void print_error(const FieldDescriptor* field, const char* format, ...) { const Descriptor* message = field->containing_type(); const FileDescriptor* file = message->file(); SourceLocation loc; if (field->GetSourceLocation(&loc)) { - // TODO: this will work if we can figure out how to pass --include_source_info to protoc + // TODO: this will work if we can figure out how to pass + // --include_source_info to protoc fprintf(stderr, "%s:%d: ", file->name().c_str(), loc.start_line); } else { fprintf(stderr, "%s: ", file->name().c_str()); @@ -84,15 +80,13 @@ print_error(const FieldDescriptor* field, const char* format, ...) va_list args; va_start(args, format); vfprintf(stderr, format, args); - va_end (args); + va_end(args); } /** * Convert a protobuf type into a java type. */ -static java_type_t -java_type(const FieldDescriptor* field) -{ +static java_type_t java_type(const FieldDescriptor* field) { int protoType = field->type(); switch (protoType) { case FieldDescriptor::TYPE_DOUBLE: @@ -117,12 +111,10 @@ java_type(const FieldDescriptor* field) return JAVA_TYPE_UNKNOWN; case FieldDescriptor::TYPE_MESSAGE: // TODO: not the final package name - if (field->message_type()->full_name() == - "android.os.statsd.AttributionNode") { - return JAVA_TYPE_ATTRIBUTION_CHAIN; - } else if (field->message_type()->full_name() == - "android.os.statsd.KeyValuePair") { - return JAVA_TYPE_KEY_VALUE_PAIR; + if (field->message_type()->full_name() == "android.os.statsd.AttributionNode") { + return JAVA_TYPE_ATTRIBUTION_CHAIN; + } else if (field->message_type()->full_name() == "android.os.statsd.KeyValuePair") { + return JAVA_TYPE_KEY_VALUE_PAIR; } else if (field->options().GetExtension(os::statsd::log_mode) == os::statsd::LogMode::MODE_BYTES) { return JAVA_TYPE_BYTE_ARRAY; @@ -151,210 +143,307 @@ java_type(const FieldDescriptor* field) /** * Gather the enums info. */ -void collate_enums(const EnumDescriptor &enumDescriptor, AtomField *atomField) { +void collate_enums(const EnumDescriptor& enumDescriptor, AtomField* atomField) { for (int i = 0; i < enumDescriptor.value_count(); i++) { atomField->enumValues[enumDescriptor.value(i)->number()] = - enumDescriptor.value(i)->name().c_str(); + enumDescriptor.value(i)->name().c_str(); } } -/** - * Gather the info about an atom proto. - */ -int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, - vector<java_type_t> *signature) { - - int errorCount = 0; - - // Build a sorted list of the fields. Descriptor has them in source file - // order. - map<int, const FieldDescriptor *> fields; - for (int j = 0; j < atom->field_count(); j++) { - const FieldDescriptor *field = atom->field(j); - fields[field->number()] = field; - } - - // Check that the parameters start at 1 and go up sequentially. - int expectedNumber = 1; - for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); - it != fields.end(); it++) { - const int number = it->first; - const FieldDescriptor *field = it->second; - if (number != expectedNumber) { - print_error(field, - "Fields must be numbered consecutively starting at 1:" - " '%s' is %d but should be %d\n", - field->name().c_str(), number, expectedNumber); - errorCount++; - expectedNumber = number; - continue; - } - expectedNumber++; - } - - // Check that only allowed types are present. Remove any invalid ones. - for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); - it != fields.end(); it++) { - const FieldDescriptor *field = it->second; - bool isBinaryField = field->options().GetExtension(os::statsd::log_mode) == - os::statsd::LogMode::MODE_BYTES; - - java_type_t javaType = java_type(field); - - if (javaType == JAVA_TYPE_UNKNOWN) { - print_error(field, "Unkown type for field: %s\n", field->name().c_str()); - errorCount++; - continue; - } else if (javaType == JAVA_TYPE_OBJECT && - atomDecl->code < PULL_ATOM_START_ID) { - // Allow attribution chain, but only at position 1. - print_error(field, - "Message type not allowed for field in pushed atoms: %s\n", - field->name().c_str()); - errorCount++; - continue; - } else if (javaType == JAVA_TYPE_BYTE_ARRAY && !isBinaryField) { - print_error(field, "Raw bytes type not allowed for field: %s\n", - field->name().c_str()); - errorCount++; - continue; +static void addAnnotationToAtomDecl(AtomDecl* atomDecl, const int fieldNumber, + const AnnotationId annotationId, + const AnnotationType annotationType, + const AnnotationValue annotationValue) { + if (dbg) { + printf(" Adding annotation to %s: [%d] = {id: %d, type: %d}\n", atomDecl->name.c_str(), + fieldNumber, annotationId, annotationType); } + atomDecl->fieldNumberToAnnotations[fieldNumber].insert( + make_shared<Annotation>(annotationId, atomDecl->code, annotationType, annotationValue)); +} - if (isBinaryField && javaType != JAVA_TYPE_BYTE_ARRAY) { - print_error(field, "Cannot mark field %s as bytes.\n", - field->name().c_str()); - errorCount++; - continue; - } +static int collate_field_annotations(AtomDecl* atomDecl, const FieldDescriptor* field, + const int fieldNumber, const java_type_t& javaType) { + int errorCount = 0; + + if (field->options().HasExtension(os::statsd::state_field_option)) { + const os::statsd::StateAtomFieldOption& stateFieldOption = + field->options().GetExtension(os::statsd::state_field_option); + const bool primaryField = stateFieldOption.primary_field(); + const bool exclusiveState = stateFieldOption.exclusive_state(); + const bool primaryFieldFirstUid = stateFieldOption.primary_field_first_uid(); + + // Check the field is only one of primaryField, exclusiveState, or primaryFieldFirstUid. + if (primaryField + primaryFieldFirstUid + exclusiveState > 1) { + print_error(field, + "Field can be max 1 of primary_field, exclusive_state, " + "or primary_field_first_uid: '%s'\n", + atomDecl->message.c_str()); + errorCount++; + } - // Doubles are not supported yet. - if (javaType == JAVA_TYPE_DOUBLE) { - print_error(field, "Doubles are not supported in atoms. Please change field %s to float\n", - field->name().c_str()); - errorCount++; - continue; - } - } - - // Check that if there's an attribution chain, it's at position 1. - for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); - it != fields.end(); it++) { - int number = it->first; - if (number != 1) { - const FieldDescriptor *field = it->second; - java_type_t javaType = java_type(field); - if (javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { - print_error( - field, - "AttributionChain fields must have field id 1, in message: '%s'\n", - atom->name().c_str()); - errorCount++; - } + if (primaryField) { + if (javaType == JAVA_TYPE_UNKNOWN || javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || + javaType == JAVA_TYPE_OBJECT || javaType == JAVA_TYPE_BYTE_ARRAY) { + print_error(field, "Invalid primary state field: '%s'\n", + atomDecl->message.c_str()); + errorCount++; + } else { + atomDecl->primaryFields.push_back(fieldNumber); + addAnnotationToAtomDecl(atomDecl, fieldNumber, ANNOTATION_ID_PRIMARY_FIELD, + ANNOTATION_TYPE_BOOL, AnnotationValue(true)); + } + } + + if (primaryFieldFirstUid) { + if (javaType != JAVA_TYPE_ATTRIBUTION_CHAIN) { + print_error(field, + "PRIMARY_FIELD_FIRST_UID annotation is only for AttributionChains: " + "'%s'\n", + atomDecl->message.c_str()); + errorCount++; + } else { + atomDecl->primaryFields.push_back(FIRST_UID_IN_CHAIN_ID); + addAnnotationToAtomDecl(atomDecl, fieldNumber, + ANNOTATION_ID_PRIMARY_FIELD_FIRST_UID, ANNOTATION_TYPE_BOOL, + AnnotationValue(true)); + } + } + + if (exclusiveState) { + if (javaType == JAVA_TYPE_UNKNOWN || javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || + javaType == JAVA_TYPE_OBJECT || javaType == JAVA_TYPE_BYTE_ARRAY) { + print_error(field, "Invalid exclusive state field: '%s'\n", + atomDecl->message.c_str()); + errorCount++; + } + + if (atomDecl->exclusiveField != 0) { + print_error(field, + "Cannot have more than one exclusive state field in an " + "atom: '%s'\n", + atomDecl->message.c_str()); + errorCount++; + } else { + atomDecl->exclusiveField = fieldNumber; + addAnnotationToAtomDecl(atomDecl, fieldNumber, ANNOTATION_ID_EXCLUSIVE_STATE, + ANNOTATION_TYPE_BOOL, AnnotationValue(true)); + } + + if (stateFieldOption.has_default_state_value()) { + const int defaultState = stateFieldOption.default_state_value(); + atomDecl->defaultState = defaultState; + + addAnnotationToAtomDecl(atomDecl, fieldNumber, ANNOTATION_ID_DEFAULT_STATE, + ANNOTATION_TYPE_INT, AnnotationValue(defaultState)); + } + + if (stateFieldOption.has_trigger_state_reset_value()) { + const int triggerStateReset = stateFieldOption.trigger_state_reset_value(); + + atomDecl->triggerStateReset = triggerStateReset; + addAnnotationToAtomDecl(atomDecl, fieldNumber, ANNOTATION_ID_TRIGGER_STATE_RESET, + ANNOTATION_TYPE_INT, AnnotationValue(triggerStateReset)); + } + + if (stateFieldOption.has_nested()) { + const bool nested = stateFieldOption.nested(); + atomDecl->nested = nested; + + addAnnotationToAtomDecl(atomDecl, fieldNumber, ANNOTATION_ID_STATE_NESTED, + ANNOTATION_TYPE_BOOL, AnnotationValue(nested)); + } + } } - } - - // Build the type signature and the atom data. - for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); - it != fields.end(); it++) { - const FieldDescriptor *field = it->second; - java_type_t javaType = java_type(field); - bool isBinaryField = field->options().GetExtension(os::statsd::log_mode) == - os::statsd::LogMode::MODE_BYTES; - - AtomField atField(field->name(), javaType); - // Generate signature for pushed atoms - if (atomDecl->code < PULL_ATOM_START_ID) { - if (javaType == JAVA_TYPE_ENUM) { - // All enums are treated as ints when it comes to function signatures. - signature->push_back(JAVA_TYPE_INT); - collate_enums(*field->enum_type(), &atField); - } else if (javaType == JAVA_TYPE_OBJECT && isBinaryField) { - signature->push_back(JAVA_TYPE_BYTE_ARRAY); - } else { - signature->push_back(javaType); - } + + if (field->options().GetExtension(os::statsd::is_uid) == true) { + if (javaType != JAVA_TYPE_INT) { + print_error(field, "is_uid annotation can only be applied to int32 fields: '%s'\n", + atomDecl->message.c_str()); + errorCount++; + } + + if (atomDecl->uidField == 0) { + atomDecl->uidField = fieldNumber; + + addAnnotationToAtomDecl(atomDecl, fieldNumber, ANNOTATION_ID_IS_UID, + ANNOTATION_TYPE_BOOL, AnnotationValue(true)); + } else { + print_error(field, + "Cannot have more than one field in an atom with is_uid " + "annotation: '%s'\n", + atomDecl->message.c_str()); + errorCount++; + } } - if (javaType == JAVA_TYPE_ENUM) { - // All enums are treated as ints when it comes to function signatures. - collate_enums(*field->enum_type(), &atField); + + return errorCount; +} + +/** + * Gather the info about an atom proto. + */ +int collate_atom(const Descriptor* atom, AtomDecl* atomDecl, vector<java_type_t>* signature) { + int errorCount = 0; + + // Build a sorted list of the fields. Descriptor has them in source file + // order. + map<int, const FieldDescriptor*> fields; + for (int j = 0; j < atom->field_count(); j++) { + const FieldDescriptor* field = atom->field(j); + fields[field->number()] = field; } - atomDecl->fields.push_back(atField); - if (field->options().GetExtension(os::statsd::state_field_option).option() == - os::statsd::StateField::PRIMARY) { - if (javaType == JAVA_TYPE_UNKNOWN || - javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || - javaType == JAVA_TYPE_OBJECT || javaType == JAVA_TYPE_BYTE_ARRAY) { + // Check that the parameters start at 1 and go up sequentially. + int expectedNumber = 1; + for (map<int, const FieldDescriptor*>::const_iterator it = fields.begin(); it != fields.end(); + it++) { + const int number = it->first; + const FieldDescriptor* field = it->second; + if (number != expectedNumber) { + print_error(field, + "Fields must be numbered consecutively starting at 1:" + " '%s' is %d but should be %d\n", + field->name().c_str(), number, expectedNumber); errorCount++; + expectedNumber = number; + continue; } - atomDecl->primaryFields.push_back(it->first); + expectedNumber++; } - if (field->options().GetExtension(os::statsd::state_field_option).option() == - os::statsd::StateField::EXCLUSIVE) { - if (javaType == JAVA_TYPE_UNKNOWN || - javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || - javaType == JAVA_TYPE_OBJECT || javaType == JAVA_TYPE_BYTE_ARRAY) { + // Check that only allowed types are present. Remove any invalid ones. + for (map<int, const FieldDescriptor*>::const_iterator it = fields.begin(); it != fields.end(); + it++) { + const FieldDescriptor* field = it->second; + bool isBinaryField = field->options().GetExtension(os::statsd::log_mode) == + os::statsd::LogMode::MODE_BYTES; + + java_type_t javaType = java_type(field); + + if (javaType == JAVA_TYPE_UNKNOWN) { + print_error(field, "Unknown type for field: %s\n", field->name().c_str()); + errorCount++; + continue; + } else if (javaType == JAVA_TYPE_OBJECT && atomDecl->code < PULL_ATOM_START_ID) { + // Allow attribution chain, but only at position 1. + print_error(field, "Message type not allowed for field in pushed atoms: %s\n", + field->name().c_str()); + errorCount++; + continue; + } else if (javaType == JAVA_TYPE_BYTE_ARRAY && !isBinaryField) { + print_error(field, "Raw bytes type not allowed for field: %s\n", field->name().c_str()); errorCount++; + continue; } - if (atomDecl->exclusiveField == 0) { - atomDecl->exclusiveField = it->first; - } else { + if (isBinaryField && javaType != JAVA_TYPE_BYTE_ARRAY) { + print_error(field, "Cannot mark field %s as bytes.\n", field->name().c_str()); errorCount++; + continue; } - } - if (field->options().GetExtension(os::statsd::is_uid) == true) { - if (javaType != JAVA_TYPE_INT) { + // Doubles are not supported yet. + if (javaType == JAVA_TYPE_DOUBLE) { + print_error(field, + "Doubles are not supported in atoms. Please change field %s " + "to float\n", + field->name().c_str()); errorCount++; + continue; } - if (atomDecl->uidField == 0) { - atomDecl->uidField = it->first; - } else { + if (field->is_repeated() && + !(javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || javaType == JAVA_TYPE_KEY_VALUE_PAIR)) { + print_error(field, + "Repeated fields are not supported in atoms. Please make " + "field %s not " + "repeated.\n", + field->name().c_str()); errorCount++; + continue; } } - // Binary field validity is already checked above. - if (isBinaryField) { - atomDecl->binaryFields.push_back(it->first); + + // Check that if there's an attribution chain, it's at position 1. + for (map<int, const FieldDescriptor*>::const_iterator it = fields.begin(); it != fields.end(); + it++) { + int number = it->first; + if (number != 1) { + const FieldDescriptor* field = it->second; + java_type_t javaType = java_type(field); + if (javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { + print_error(field, + "AttributionChain fields must have field id 1, in message: '%s'\n", + atom->name().c_str()); + errorCount++; + } + } + } + + // Build the type signature and the atom data. + for (map<int, const FieldDescriptor*>::const_iterator it = fields.begin(); it != fields.end(); + it++) { + const FieldDescriptor* field = it->second; + java_type_t javaType = java_type(field); + bool isBinaryField = field->options().GetExtension(os::statsd::log_mode) == + os::statsd::LogMode::MODE_BYTES; + + AtomField atField(field->name(), javaType); + + if (javaType == JAVA_TYPE_ENUM) { + // All enums are treated as ints when it comes to function signatures. + collate_enums(*field->enum_type(), &atField); + } + + // Generate signature for pushed atoms + if (atomDecl->code < PULL_ATOM_START_ID) { + if (javaType == JAVA_TYPE_ENUM) { + // All enums are treated as ints when it comes to function signatures. + signature->push_back(JAVA_TYPE_INT); + } else if (javaType == JAVA_TYPE_OBJECT && isBinaryField) { + signature->push_back(JAVA_TYPE_BYTE_ARRAY); + } else { + signature->push_back(javaType); + } + } + + atomDecl->fields.push_back(atField); + + errorCount += collate_field_annotations(atomDecl, field, it->first, javaType); } - } - return errorCount; + return errorCount; } -// This function flattens the fields of the AttributionNode proto in an Atom proto and generates -// the corresponding atom decl and signature. -bool get_non_chained_node(const Descriptor *atom, AtomDecl *atomDecl, - vector<java_type_t> *signature) { +// This function flattens the fields of the AttributionNode proto in an Atom +// proto and generates the corresponding atom decl and signature. +bool get_non_chained_node(const Descriptor* atom, AtomDecl* atomDecl, + vector<java_type_t>* signature) { // Build a sorted list of the fields. Descriptor has them in source file // order. - map<int, const FieldDescriptor *> fields; + map<int, const FieldDescriptor*> fields; for (int j = 0; j < atom->field_count(); j++) { - const FieldDescriptor *field = atom->field(j); + const FieldDescriptor* field = atom->field(j); fields[field->number()] = field; } AtomDecl attributionDecl; vector<java_type_t> attributionSignature; - collate_atom(android::os::statsd::AttributionNode::descriptor(), - &attributionDecl, &attributionSignature); + collate_atom(android::os::statsd::AttributionNode::descriptor(), &attributionDecl, + &attributionSignature); // Build the type signature and the atom data. bool has_attribution_node = false; - for (map<int, const FieldDescriptor *>::const_iterator it = fields.begin(); - it != fields.end(); it++) { - const FieldDescriptor *field = it->second; + for (map<int, const FieldDescriptor*>::const_iterator it = fields.begin(); it != fields.end(); + it++) { + const FieldDescriptor* field = it->second; java_type_t javaType = java_type(field); if (javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { - atomDecl->fields.insert( - atomDecl->fields.end(), - attributionDecl.fields.begin(), attributionDecl.fields.end()); - signature->insert( - signature->end(), - attributionSignature.begin(), attributionSignature.end()); + atomDecl->fields.insert(atomDecl->fields.end(), attributionDecl.fields.begin(), + attributionDecl.fields.end()); + signature->insert(signature->end(), attributionSignature.begin(), + attributionSignature.end()); has_attribution_node = true; } else { @@ -372,106 +461,115 @@ bool get_non_chained_node(const Descriptor *atom, AtomDecl *atomDecl, return has_attribution_node; } +static void populateFieldNumberToAtomDeclSet(const shared_ptr<AtomDecl>& atomDecl, + FieldNumberToAtomDeclSet* fieldNumberToAtomDeclSet) { + for (FieldNumberToAnnotations::const_iterator it = atomDecl->fieldNumberToAnnotations.begin(); + it != atomDecl->fieldNumberToAnnotations.end(); it++) { + const int fieldNumber = it->first; + (*fieldNumberToAtomDeclSet)[fieldNumber].insert(atomDecl); + } +} + /** * Gather the info about the atoms. */ -int collate_atoms(const Descriptor *descriptor, Atoms *atoms) { - int errorCount = 0; - const bool dbg = false; +int collate_atoms(const Descriptor* descriptor, const string& moduleName, Atoms* atoms) { + int errorCount = 0; + + for (int i = 0; i < descriptor->field_count(); i++) { + const FieldDescriptor* atomField = descriptor->field(i); + + if (moduleName != DEFAULT_MODULE_NAME) { + const int moduleCount = atomField->options().ExtensionSize(os::statsd::module); + int j; + for (j = 0; j < moduleCount; ++j) { + const string atomModuleName = + atomField->options().GetExtension(os::statsd::module, j); + if (atomModuleName == moduleName) { + break; + } + } - int maxPushedAtomId = 2; - for (int i = 0; i < descriptor->field_count(); i++) { - const FieldDescriptor *atomField = descriptor->field(i); + // This atom is not in the module we're interested in; skip it. + if (moduleCount == j) { + if (dbg) { + printf(" Skipping %s (%d)\n", atomField->name().c_str(), atomField->number()); + } + continue; + } + } - if (dbg) { - printf(" %s (%d)\n", atomField->name().c_str(), atomField->number()); - } + if (dbg) { + printf(" %s (%d)\n", atomField->name().c_str(), atomField->number()); + } - // StatsEvent only has one oneof, which contains only messages. Don't allow - // other types. - if (atomField->type() != FieldDescriptor::TYPE_MESSAGE) { - print_error(atomField, - "Bad type for atom. StatsEvent can only have message type " - "fields: %s\n", - atomField->name().c_str()); - errorCount++; - continue; - } + // StatsEvent only has one oneof, which contains only messages. Don't allow + // other types. + if (atomField->type() != FieldDescriptor::TYPE_MESSAGE) { + print_error(atomField, + "Bad type for atom. StatsEvent can only have message type " + "fields: %s\n", + atomField->name().c_str()); + errorCount++; + continue; + } - const Descriptor *atom = atomField->message_type(); - AtomDecl atomDecl(atomField->number(), atomField->name(), atom->name()); + const Descriptor* atom = atomField->message_type(); + shared_ptr<AtomDecl> atomDecl = + make_shared<AtomDecl>(atomField->number(), atomField->name(), atom->name()); + + if (atomDecl->code < PULL_ATOM_START_ID && + atomField->options().GetExtension(os::statsd::truncate_timestamp)) { + addAnnotationToAtomDecl(atomDecl.get(), ATOM_ID_FIELD_NUMBER, + ANNOTATION_ID_TRUNCATE_TIMESTAMP, ANNOTATION_TYPE_BOOL, + AnnotationValue(true)); + if (dbg) { + printf("%s can have timestamp truncated\n", atomField->name().c_str()); + } + } - if (atomField->options().GetExtension(os::statsd::allow_from_any_uid) == true) { - atomDecl.whitelisted = true; - } + vector<java_type_t> signature; + errorCount += collate_atom(atom, atomDecl.get(), &signature); + if (atomDecl->primaryFields.size() != 0 && atomDecl->exclusiveField == 0) { + print_error(atomField, "Cannot have a primary field without an exclusive field: %s\n", + atomField->name().c_str()); + errorCount++; + continue; + } - if (atomField->options().HasExtension(os::statsd::log_from_module)) { - atomDecl.hasModule = true; - atomDecl.moduleName = atomField->options().GetExtension(os::statsd::log_from_module); - } + FieldNumberToAtomDeclSet& fieldNumberToAtomDeclSet = atoms->signatureInfoMap[signature]; + populateFieldNumberToAtomDeclSet(atomDecl, &fieldNumberToAtomDeclSet); - vector<java_type_t> signature; - errorCount += collate_atom(atom, &atomDecl, &signature); - if (atomDecl.primaryFields.size() != 0 && atomDecl.exclusiveField == 0) { - errorCount++; - } + atoms->decls.insert(atomDecl); - // Add the signature if does not already exist. - auto signature_to_modules_it = atoms->signatures_to_modules.find(signature); - if (signature_to_modules_it == atoms->signatures_to_modules.end()) { - set<string> modules; - if (atomDecl.hasModule) { - modules.insert(atomDecl.moduleName); - } - atoms->signatures_to_modules[signature] = modules; - } else { - if (atomDecl.hasModule) { - signature_to_modules_it->second.insert(atomDecl.moduleName); + shared_ptr<AtomDecl> nonChainedAtomDecl = + make_shared<AtomDecl>(atomField->number(), atomField->name(), atom->name()); + vector<java_type_t> nonChainedSignature; + if (get_non_chained_node(atom, nonChainedAtomDecl.get(), &nonChainedSignature)) { + FieldNumberToAtomDeclSet& nonChainedFieldNumberToAtomDeclSet = + atoms->nonChainedSignatureInfoMap[nonChainedSignature]; + populateFieldNumberToAtomDeclSet(nonChainedAtomDecl, + &nonChainedFieldNumberToAtomDeclSet); + + atoms->non_chained_decls.insert(nonChainedAtomDecl); } } - atoms->decls.insert(atomDecl); - - AtomDecl nonChainedAtomDecl(atomField->number(), atomField->name(), atom->name()); - vector<java_type_t> nonChainedSignature; - if (get_non_chained_node(atom, &nonChainedAtomDecl, &nonChainedSignature)) { - auto it = atoms->non_chained_signatures_to_modules.find(nonChainedSignature); - if (it == atoms->non_chained_signatures_to_modules.end()) { - set<string> modules_non_chained; - if (atomDecl.hasModule) { - modules_non_chained.insert(atomDecl.moduleName); - } - atoms->non_chained_signatures_to_modules[nonChainedSignature] = modules_non_chained; - } else { - if (atomDecl.hasModule) { - it->second.insert(atomDecl.moduleName); + + if (dbg) { + printf("signatures = [\n"); + for (SignatureInfoMap::const_iterator it = atoms->signatureInfoMap.begin(); + it != atoms->signatureInfoMap.end(); it++) { + printf(" "); + for (vector<java_type_t>::const_iterator jt = it->first.begin(); jt != it->first.end(); + jt++) { + printf(" %d", (int)*jt); } + printf("\n"); } - atoms->non_chained_decls.insert(nonChainedAtomDecl); - } - - if (atomDecl.code < PULL_ATOM_START_ID && atomDecl.code > maxPushedAtomId) { - maxPushedAtomId = atomDecl.code; - } - } - - atoms->maxPushedAtomId = maxPushedAtomId; - - if (dbg) { - printf("signatures = [\n"); - for (map<vector<java_type_t>, set<string>>::const_iterator it = - atoms->signatures_to_modules.begin(); - it != atoms->signatures_to_modules.end(); it++) { - printf(" "); - for (vector<java_type_t>::const_iterator jt = it->first.begin(); - jt != it->first.end(); jt++) { - printf(" %d", (int)*jt); - } - printf("\n"); + printf("]\n"); } - printf("]\n"); - } - return errorCount; + return errorCount; } } // namespace stats_log_api_gen diff --git a/tools/stats_log_api_gen/Collation.h b/tools/stats_log_api_gen/Collation.h index 3efdd520d7f5..10b34ecf5f54 100644 --- a/tools/stats_log_api_gen/Collation.h +++ b/tools/stats_log_api_gen/Collation.h @@ -17,45 +17,110 @@ #ifndef ANDROID_STATS_LOG_API_GEN_COLLATION_H #define ANDROID_STATS_LOG_API_GEN_COLLATION_H - #include <google/protobuf/descriptor.h> +#include <stdint.h> +#include <map> #include <set> #include <vector> -#include <map> + +#include "frameworks/base/cmds/statsd/src/atom_field_options.pb.h" namespace android { namespace stats_log_api_gen { +using google::protobuf::Descriptor; +using google::protobuf::FieldDescriptor; using std::map; using std::set; +using std::shared_ptr; using std::string; using std::vector; -using google::protobuf::Descriptor; -using google::protobuf::FieldDescriptor; const int PULL_ATOM_START_ID = 10000; +const int FIRST_UID_IN_CHAIN_ID = 0; + +enum AnnotationId : uint8_t { + ANNOTATION_ID_IS_UID = 1, + ANNOTATION_ID_TRUNCATE_TIMESTAMP = 2, + ANNOTATION_ID_PRIMARY_FIELD = 3, + ANNOTATION_ID_EXCLUSIVE_STATE = 4, + ANNOTATION_ID_PRIMARY_FIELD_FIRST_UID = 5, + ANNOTATION_ID_DEFAULT_STATE = 6, + ANNOTATION_ID_TRIGGER_STATE_RESET = 7, + ANNOTATION_ID_STATE_NESTED = 8, +}; + +const int ATOM_ID_FIELD_NUMBER = -1; + +const string DEFAULT_MODULE_NAME = "DEFAULT"; + /** * The types for atom parameters. */ typedef enum { - JAVA_TYPE_UNKNOWN = 0, - - JAVA_TYPE_ATTRIBUTION_CHAIN = 1, - JAVA_TYPE_BOOLEAN = 2, - JAVA_TYPE_INT = 3, - JAVA_TYPE_LONG = 4, - JAVA_TYPE_FLOAT = 5, - JAVA_TYPE_DOUBLE = 6, - JAVA_TYPE_STRING = 7, - JAVA_TYPE_ENUM = 8, - JAVA_TYPE_KEY_VALUE_PAIR = 9, - - JAVA_TYPE_OBJECT = -1, - JAVA_TYPE_BYTE_ARRAY = -2, + JAVA_TYPE_UNKNOWN = 0, + + JAVA_TYPE_ATTRIBUTION_CHAIN = 1, + JAVA_TYPE_BOOLEAN = 2, + JAVA_TYPE_INT = 3, + JAVA_TYPE_LONG = 4, + JAVA_TYPE_FLOAT = 5, + JAVA_TYPE_DOUBLE = 6, + JAVA_TYPE_STRING = 7, + JAVA_TYPE_ENUM = 8, + JAVA_TYPE_KEY_VALUE_PAIR = 9, + + JAVA_TYPE_OBJECT = -1, + JAVA_TYPE_BYTE_ARRAY = -2, } java_type_t; +enum AnnotationType { + ANNOTATION_TYPE_UNKNOWN = 0, + ANNOTATION_TYPE_INT = 1, + ANNOTATION_TYPE_BOOL = 2, +}; + +union AnnotationValue { + int intValue; + bool boolValue; + + AnnotationValue(const int value) : intValue(value) { + } + AnnotationValue(const bool value) : boolValue(value) { + } +}; + +struct Annotation { + const AnnotationId annotationId; + const int atomId; + AnnotationType type; + AnnotationValue value; + + inline Annotation(AnnotationId annotationId, int atomId, AnnotationType type, + AnnotationValue value) + : annotationId(annotationId), atomId(atomId), type(type), value(value) { + } + inline ~Annotation() { + } + + inline bool operator<(const Annotation& that) const { + return atomId == that.atomId ? annotationId < that.annotationId : atomId < that.atomId; + } +}; + +struct SharedComparator { + template <typename T> + inline bool operator()(const shared_ptr<T>& lhs, const shared_ptr<T>& rhs) const { + return (*lhs) < (*rhs); + } +}; + +using AnnotationSet = set<shared_ptr<Annotation>, SharedComparator>; + +using FieldNumberToAnnotations = map<int, AnnotationSet>; + /** * The name and type for an atom field. */ @@ -63,15 +128,20 @@ struct AtomField { string name; java_type_t javaType; - // If the field is of type enum, the following map contains the list of enum values. + // If the field is of type enum, the following map contains the list of enum + // values. map<int /* numeric value */, string /* value name */> enumValues; - inline AtomField() :name(), javaType(JAVA_TYPE_UNKNOWN) {} - inline AtomField(const AtomField& that) :name(that.name), - javaType(that.javaType), - enumValues(that.enumValues) {} - inline AtomField(string n, java_type_t jt) :name(n), javaType(jt) {} - inline ~AtomField() {} + inline AtomField() : name(), javaType(JAVA_TYPE_UNKNOWN) { + } + inline AtomField(const AtomField& that) + : name(that.name), javaType(that.javaType), enumValues(that.enumValues) { + } + + inline AtomField(string n, java_type_t jt) : name(n), javaType(jt) { + } + inline ~AtomField() { + } }; /** @@ -84,18 +154,16 @@ struct AtomDecl { string message; vector<AtomField> fields; + FieldNumberToAnnotations fieldNumberToAnnotations; + vector<int> primaryFields; int exclusiveField = 0; + int defaultState = INT_MAX; + int triggerStateReset = INT_MAX; + bool nested = true; int uidField = 0; - bool whitelisted = false; - - vector<int> binaryFields; - - bool hasModule = false; - string moduleName; - AtomDecl(); AtomDecl(const AtomDecl& that); AtomDecl(int code, const string& name, const string& message); @@ -106,22 +174,28 @@ struct AtomDecl { } }; +using AtomDeclSet = set<shared_ptr<AtomDecl>, SharedComparator>; + +// Maps a field number to a set of atoms that have annotation(s) for their field with that field +// number. +using FieldNumberToAtomDeclSet = map<int, AtomDeclSet>; + +using SignatureInfoMap = map<vector<java_type_t>, FieldNumberToAtomDeclSet>; + struct Atoms { - map<vector<java_type_t>, set<string>> signatures_to_modules; - set<AtomDecl> decls; - set<AtomDecl> non_chained_decls; - map<vector<java_type_t>, set<string>> non_chained_signatures_to_modules; - int maxPushedAtomId; + SignatureInfoMap signatureInfoMap; + AtomDeclSet decls; + AtomDeclSet non_chained_decls; + SignatureInfoMap nonChainedSignatureInfoMap; }; /** * Gather the information about the atoms. Returns the number of errors. */ -int collate_atoms(const Descriptor* descriptor, Atoms* atoms); -int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, vector<java_type_t> *signature); +int collate_atoms(const Descriptor* descriptor, const string& moduleName, Atoms* atoms); +int collate_atom(const Descriptor* atom, AtomDecl* atomDecl, vector<java_type_t>* signature); } // namespace stats_log_api_gen } // namespace android - -#endif // ANDROID_STATS_LOG_API_GEN_COLLATION_H +#endif // ANDROID_STATS_LOG_API_GEN_COLLATION_H diff --git a/tools/stats_log_api_gen/atoms_info_writer.cpp b/tools/stats_log_api_gen/atoms_info_writer.cpp deleted file mode 100644 index 54a9982bb5c2..000000000000 --- a/tools/stats_log_api_gen/atoms_info_writer.cpp +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (C) 2019, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "atoms_info_writer.h" -#include "utils.h" - -#include <map> -#include <set> -#include <vector> - -namespace android { -namespace stats_log_api_gen { - -static void write_atoms_info_header_body(FILE* out, const Atoms& atoms) { - fprintf(out, "struct StateAtomFieldOptions {\n"); - fprintf(out, " std::vector<int> primaryFields;\n"); - fprintf(out, " int exclusiveField;\n"); - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, "struct AtomsInfo {\n"); - fprintf(out, - " const static std::set<int> " - "kTruncatingTimestampAtomBlackList;\n"); - fprintf(out, " const static std::map<int, int> kAtomsWithUidField;\n"); - fprintf(out, - " const static std::set<int> kAtomsWithAttributionChain;\n"); - fprintf(out, - " const static std::map<int, StateAtomFieldOptions> " - "kStateAtomsFieldOptions;\n"); - fprintf(out, - " const static std::map<int, std::vector<int>> " - "kBytesFieldAtoms;\n"); - fprintf(out, - " const static std::set<int> kWhitelistedAtoms;\n"); - fprintf(out, "};\n"); - fprintf(out, "const static int kMaxPushedAtomId = %d;\n\n", atoms.maxPushedAtomId); - -} - -static void write_atoms_info_cpp_body(FILE* out, const Atoms& atoms) { - std::set<string> kTruncatingAtomNames = {"mobile_radio_power_state_changed", - "audio_state_changed", - "call_state_changed", - "phone_signal_strength_changed", - "mobile_bytes_transfer_by_fg_bg", - "mobile_bytes_transfer"}; - fprintf(out, - "const std::set<int> " - "AtomsInfo::kTruncatingTimestampAtomBlackList = {\n"); - for (set<string>::const_iterator blacklistedAtom = kTruncatingAtomNames.begin(); - blacklistedAtom != kTruncatingAtomNames.end(); blacklistedAtom++) { - fprintf(out, " %s,\n", make_constant_name(*blacklistedAtom).c_str()); - } - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, - "const std::set<int> AtomsInfo::kAtomsWithAttributionChain = {\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - for (vector<AtomField>::const_iterator field = atom->fields.begin(); - field != atom->fields.end(); field++) { - if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { - string constant = make_constant_name(atom->name); - fprintf(out, " %s,\n", constant.c_str()); - break; - } - } - } - - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, - "const std::set<int> AtomsInfo::kWhitelistedAtoms = {\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->whitelisted) { - string constant = make_constant_name(atom->name); - fprintf(out, " %s,\n", constant.c_str()); - } - } - - fprintf(out, "};\n"); - fprintf(out, "\n"); - - fprintf(out, "static std::map<int, int> getAtomUidField() {\n"); - fprintf(out, " std::map<int, int> uidField;\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->uidField == 0) { - continue; - } - fprintf(out, - "\n // Adding uid field for atom " - "(%d)%s\n", - atom->code, atom->name.c_str()); - fprintf(out, " uidField[static_cast<int>(%s)] = %d;\n", - make_constant_name(atom->name).c_str(), atom->uidField); - } - - fprintf(out, " return uidField;\n"); - fprintf(out, "};\n"); - - fprintf(out, - "const std::map<int, int> AtomsInfo::kAtomsWithUidField = " - "getAtomUidField();\n"); - - fprintf(out, - "static std::map<int, StateAtomFieldOptions> " - "getStateAtomFieldOptions() {\n"); - fprintf(out, " std::map<int, StateAtomFieldOptions> options;\n"); - fprintf(out, " StateAtomFieldOptions opt;\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->primaryFields.size() == 0 && atom->exclusiveField == 0) { - continue; - } - fprintf(out, - "\n // Adding primary and exclusive fields for atom " - "(%d)%s\n", - atom->code, atom->name.c_str()); - fprintf(out, " opt.primaryFields.clear();\n"); - for (const auto& field : atom->primaryFields) { - fprintf(out, " opt.primaryFields.push_back(%d);\n", field); - } - - fprintf(out, " opt.exclusiveField = %d;\n", atom->exclusiveField); - fprintf(out, " options[static_cast<int>(%s)] = opt;\n", - make_constant_name(atom->name).c_str()); - } - - fprintf(out, " return options;\n"); - fprintf(out, "}\n"); - - fprintf(out, - "const std::map<int, StateAtomFieldOptions> " - "AtomsInfo::kStateAtomsFieldOptions = " - "getStateAtomFieldOptions();\n"); - - fprintf(out, - "static std::map<int, std::vector<int>> " - "getBinaryFieldAtoms() {\n"); - fprintf(out, " std::map<int, std::vector<int>> options;\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - if (atom->binaryFields.size() == 0) { - continue; - } - fprintf(out, - "\n // Adding binary fields for atom " - "(%d)%s\n", - atom->code, atom->name.c_str()); - - for (const auto& field : atom->binaryFields) { - fprintf(out, " options[static_cast<int>(%s)].push_back(%d);\n", - make_constant_name(atom->name).c_str(), field); - } - } - - fprintf(out, " return options;\n"); - fprintf(out, "}\n"); - - fprintf(out, - "const std::map<int, std::vector<int>> " - "AtomsInfo::kBytesFieldAtoms = " - "getBinaryFieldAtoms();\n"); - -} - -int write_atoms_info_header(FILE* out, const Atoms &atoms, const string& namespaceStr) { - // Print prelude - fprintf(out, "// This file is autogenerated\n"); - fprintf(out, "\n"); - fprintf(out, "#pragma once\n"); - fprintf(out, "\n"); - fprintf(out, "#include <vector>\n"); - fprintf(out, "#include <map>\n"); - fprintf(out, "#include <set>\n"); - fprintf(out, "\n"); - - write_namespace(out, namespaceStr); - - write_atoms_info_header_body(out, atoms); - - fprintf(out, "\n"); - write_closing_namespace(out, namespaceStr); - - return 0; -} - -int write_atoms_info_cpp(FILE *out, const Atoms &atoms, const string& namespaceStr, - const string& importHeader, const string& statslogHeader) { - // Print prelude - fprintf(out, "// This file is autogenerated\n"); - fprintf(out, "\n"); - fprintf(out, "#include <%s>\n", importHeader.c_str()); - fprintf(out, "#include <%s>\n", statslogHeader.c_str()); - fprintf(out, "\n"); - - write_namespace(out, namespaceStr); - - write_atoms_info_cpp_body(out, atoms); - - // Print footer - fprintf(out, "\n"); - write_closing_namespace(out, namespaceStr); - - return 0; -} - -} // namespace stats_log_api_gen -} // namespace android diff --git a/tools/stats_log_api_gen/atoms_info_writer.h b/tools/stats_log_api_gen/atoms_info_writer.h deleted file mode 100644 index 12ac862871ef..000000000000 --- a/tools/stats_log_api_gen/atoms_info_writer.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2019, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "Collation.h" - -#include <stdio.h> -#include <string.h> - -namespace android { -namespace stats_log_api_gen { - -using namespace std; - -int write_atoms_info_cpp(FILE* out, const Atoms& atoms, const string& namespaceStr, - const string& importHeader, const string& statslogHeader); - -int write_atoms_info_header(FILE* out, const Atoms& atoms, const string& namespaceStr); - -} // namespace stats_log_api_gen -} // namespace android diff --git a/tools/stats_log_api_gen/java_writer.cpp b/tools/stats_log_api_gen/java_writer.cpp index c29936b96c14..f4c937c3f599 100644 --- a/tools/stats_log_api_gen/java_writer.cpp +++ b/tools/stats_log_api_gen/java_writer.cpp @@ -15,18 +15,15 @@ */ #include "java_writer.h" + #include "java_writer_q.h" #include "utils.h" namespace android { namespace stats_log_api_gen { -static int write_java_q_logger_class( - FILE* out, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl, - const string& moduleName - ) { +static int write_java_q_logger_class(FILE* out, const SignatureInfoMap& signatureInfoMap, + const AtomDecl& attributionDecl) { fprintf(out, "\n"); fprintf(out, " // Write logging helper methods for statsd in Q and earlier.\n"); fprintf(out, " private static class QLogger {\n"); @@ -36,41 +33,84 @@ static int write_java_q_logger_class( // Print Q write methods. fprintf(out, "\n"); fprintf(out, " // Write methods.\n"); - write_java_methods_q_schema( - out, signatures_to_modules, attributionDecl, moduleName, " "); + write_java_methods_q_schema(out, signatureInfoMap, attributionDecl, " "); fprintf(out, " }\n"); return 0; } +static void write_java_annotation_constants(FILE* out) { + fprintf(out, " // Annotation constants.\n"); + + for (const auto& [id, name] : ANNOTATION_ID_CONSTANTS) { + fprintf(out, " public static final byte %s = %hhu;\n", name.c_str(), id); + } + fprintf(out, "\n"); +} -static int write_java_methods( - FILE* out, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl, - const string& moduleName, - const bool supportQ - ) { - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - // Skip if this signature is not needed for the module. - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { - continue; +static void write_annotations(FILE* out, int argIndex, + const FieldNumberToAtomDeclSet& fieldNumberToAtomDeclSet) { + FieldNumberToAtomDeclSet::const_iterator fieldNumberToAtomDeclSetIt = + fieldNumberToAtomDeclSet.find(argIndex); + if (fieldNumberToAtomDeclSet.end() == fieldNumberToAtomDeclSetIt) { + return; + } + const AtomDeclSet& atomDeclSet = fieldNumberToAtomDeclSetIt->second; + for (const shared_ptr<AtomDecl>& atomDecl : atomDeclSet) { + const string atomConstant = make_constant_name(atomDecl->name); + fprintf(out, " if (%s == code) {\n", atomConstant.c_str()); + const AnnotationSet& annotations = atomDecl->fieldNumberToAnnotations.at(argIndex); + int resetState = -1; + int defaultState = -1; + for (const shared_ptr<Annotation>& annotation : annotations) { + const string& annotationConstant = ANNOTATION_ID_CONSTANTS.at(annotation->annotationId); + switch (annotation->type) { + case ANNOTATION_TYPE_INT: + if (ANNOTATION_ID_TRIGGER_STATE_RESET == annotation->annotationId) { + resetState = annotation->value.intValue; + } else if (ANNOTATION_ID_DEFAULT_STATE == annotation->annotationId) { + defaultState = annotation->value.intValue; + } else { + fprintf(out, " builder.addIntAnnotation(%s, %d);\n", + annotationConstant.c_str(), annotation->value.intValue); + } + break; + case ANNOTATION_TYPE_BOOL: + fprintf(out, " builder.addBooleanAnnotation(%s, %s);\n", + annotationConstant.c_str(), + annotation->value.boolValue ? "true" : "false"); + break; + default: + break; + } } + if (defaultState != -1 && resetState != -1) { + const string& annotationConstant = + ANNOTATION_ID_CONSTANTS.at(ANNOTATION_ID_TRIGGER_STATE_RESET); + fprintf(out, " if (arg%d == %d) {\n", argIndex, resetState); + fprintf(out, " builder.addIntAnnotation(%s, %d);\n", + annotationConstant.c_str(), defaultState); + fprintf(out, " }\n"); + } + fprintf(out, " }\n"); + } +} +static int write_java_methods(FILE* out, const SignatureInfoMap& signatureInfoMap, + const AtomDecl& attributionDecl, const bool supportQ) { + for (auto signatureInfoMapIt = signatureInfoMap.begin(); + signatureInfoMapIt != signatureInfoMap.end(); signatureInfoMapIt++) { // Print method signature. - if (DEFAULT_MODULE_NAME == moduleName) { - fprintf(out, " /** @hide */\n"); - } fprintf(out, " public static void write(int code"); - vector<java_type_t> signature = signature_to_modules_it->first; + const vector<java_type_t>& signature = signatureInfoMapIt->first; + const FieldNumberToAtomDeclSet& fieldNumberToAtomDeclSet = signatureInfoMapIt->second; int argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (auto chainField : attributionDecl.fields) { - fprintf(out, ", %s[] %s", - java_type_name(chainField.javaType), chainField.name.c_str()); + fprintf(out, ", %s[] %s", java_type_name(chainField.javaType), + chainField.name.c_str()); } } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { fprintf(out, ", android.util.SparseArray<Object> valueMap"); @@ -84,135 +124,130 @@ static int write_java_methods( // Print method body. string indent(""); if (supportQ) { - // TODO(b/146235828): Use just SDK_INT check once it is incremented from Q. - fprintf(out, " if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q\n"); - fprintf(out, " || (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q\n"); - fprintf(out, " && Build.VERSION.PREVIEW_SDK_INT > 0)) {\n"); + fprintf(out, " if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {\n"); indent = " "; } // Start StatsEvent.Builder. - fprintf(out, "%s final StatsEvent.Builder builder = StatsEvent.newBuilder();\n", + fprintf(out, + "%s final StatsEvent.Builder builder = " + "StatsEvent.newBuilder();\n", indent.c_str()); // Write atom code. fprintf(out, "%s builder.setAtomId(code);\n", indent.c_str()); + write_annotations(out, ATOM_ID_FIELD_NUMBER, fieldNumberToAtomDeclSet); // Write the args. argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { switch (*arg) { - case JAVA_TYPE_BOOLEAN: - fprintf(out, "%s builder.writeBoolean(arg%d);\n", indent.c_str(), argIndex); - break; - case JAVA_TYPE_INT: - case JAVA_TYPE_ENUM: - fprintf(out, "%s builder.writeInt(arg%d);\n", indent.c_str(), argIndex); - break; - case JAVA_TYPE_FLOAT: - fprintf(out, "%s builder.writeFloat(arg%d);\n", indent.c_str(), argIndex); - break; - case JAVA_TYPE_LONG: - fprintf(out, "%s builder.writeLong(arg%d);\n", indent.c_str(), argIndex); - break; - case JAVA_TYPE_STRING: - fprintf(out, "%s builder.writeString(arg%d);\n", indent.c_str(), argIndex); - break; - case JAVA_TYPE_BYTE_ARRAY: - fprintf(out, "%s builder.writeByteArray(null == arg%d ? new byte[0] : arg%d);\n", - indent.c_str(), argIndex, argIndex); - break; - case JAVA_TYPE_ATTRIBUTION_CHAIN: - { - const char* uidName = attributionDecl.fields.front().name.c_str(); - const char* tagName = attributionDecl.fields.back().name.c_str(); + case JAVA_TYPE_BOOLEAN: + fprintf(out, "%s builder.writeBoolean(arg%d);\n", indent.c_str(), + argIndex); + break; + case JAVA_TYPE_INT: + case JAVA_TYPE_ENUM: + fprintf(out, "%s builder.writeInt(arg%d);\n", indent.c_str(), argIndex); + break; + case JAVA_TYPE_FLOAT: + fprintf(out, "%s builder.writeFloat(arg%d);\n", indent.c_str(), + argIndex); + break; + case JAVA_TYPE_LONG: + fprintf(out, "%s builder.writeLong(arg%d);\n", indent.c_str(), argIndex); + break; + case JAVA_TYPE_STRING: + fprintf(out, "%s builder.writeString(arg%d);\n", indent.c_str(), + argIndex); + break; + case JAVA_TYPE_BYTE_ARRAY: + fprintf(out, + "%s builder.writeByteArray(null == arg%d ? new byte[0] : " + "arg%d);\n", + indent.c_str(), argIndex, argIndex); + break; + case JAVA_TYPE_ATTRIBUTION_CHAIN: { + const char* uidName = attributionDecl.fields.front().name.c_str(); + const char* tagName = attributionDecl.fields.back().name.c_str(); - fprintf(out, "%s builder.writeAttributionChain(\n", indent.c_str()); - fprintf(out, "%s null == %s ? new int[0] : %s,\n", - indent.c_str(), uidName, uidName); - fprintf(out, "%s null == %s ? new String[0] : %s);\n", - indent.c_str(), tagName, tagName); - break; - } - case JAVA_TYPE_KEY_VALUE_PAIR: - fprintf(out, "\n"); - fprintf(out, - "%s // Write KeyValuePairs.\n", indent.c_str()); - fprintf(out, - "%s final int count = valueMap.size();\n", indent.c_str()); - fprintf(out, - "%s android.util.SparseIntArray intMap = null;\n", - indent.c_str()); - fprintf(out, - "%s android.util.SparseLongArray longMap = null;\n", - indent.c_str()); - fprintf(out, - "%s android.util.SparseArray<String> stringMap = null;\n", - indent.c_str()); - fprintf(out, - "%s android.util.SparseArray<Float> floatMap = null;\n", - indent.c_str()); - fprintf(out, - "%s for (int i = 0; i < count; i++) {\n", indent.c_str()); - fprintf(out, - "%s final int key = valueMap.keyAt(i);\n", indent.c_str()); - fprintf(out, - "%s final Object value = valueMap.valueAt(i);\n", - indent.c_str()); - fprintf(out, - "%s if (value instanceof Integer) {\n", indent.c_str()); - fprintf(out, - "%s if (null == intMap) {\n", indent.c_str()); - fprintf(out, - "%s intMap = new android.util.SparseIntArray();\n", indent.c_str()); - fprintf(out, - "%s }\n", indent.c_str()); - fprintf(out, - "%s intMap.put(key, (Integer) value);\n", indent.c_str()); - fprintf(out, - "%s } else if (value instanceof Long) {\n", indent.c_str()); - fprintf(out, - "%s if (null == longMap) {\n", indent.c_str()); - fprintf(out, - "%s longMap = new android.util.SparseLongArray();\n", indent.c_str()); - fprintf(out, - "%s }\n", indent.c_str()); - fprintf(out, - "%s longMap.put(key, (Long) value);\n", indent.c_str()); - fprintf(out, - "%s } else if (value instanceof String) {\n", indent.c_str()); - fprintf(out, - "%s if (null == stringMap) {\n", indent.c_str()); - fprintf(out, - "%s stringMap = new android.util.SparseArray<>();\n", indent.c_str()); - fprintf(out, - "%s }\n", indent.c_str()); - fprintf(out, - "%s stringMap.put(key, (String) value);\n", indent.c_str()); - fprintf(out, - "%s } else if (value instanceof Float) {\n", indent.c_str()); - fprintf(out, - "%s if (null == floatMap) {\n", indent.c_str()); - fprintf(out, - "%s floatMap = new android.util.SparseArray<>();\n", indent.c_str()); - fprintf(out, - "%s }\n", indent.c_str()); - fprintf(out, - "%s floatMap.put(key, (Float) value);\n", indent.c_str()); - fprintf(out, - "%s }\n", indent.c_str()); - fprintf(out, - "%s }\n", indent.c_str()); - fprintf(out, - "%s builder.writeKeyValuePairs(" - "intMap, longMap, stringMap, floatMap);\n", indent.c_str()); - break; - default: - // Unsupported types: OBJECT, DOUBLE. - fprintf(stderr, "Encountered unsupported type."); - return 1; + fprintf(out, "%s builder.writeAttributionChain(\n", indent.c_str()); + fprintf(out, "%s null == %s ? new int[0] : %s,\n", + indent.c_str(), uidName, uidName); + fprintf(out, "%s null == %s ? new String[0] : %s);\n", + indent.c_str(), tagName, tagName); + break; + } + case JAVA_TYPE_KEY_VALUE_PAIR: + fprintf(out, "\n"); + fprintf(out, "%s // Write KeyValuePairs.\n", indent.c_str()); + fprintf(out, "%s final int count = valueMap.size();\n", indent.c_str()); + fprintf(out, "%s android.util.SparseIntArray intMap = null;\n", + indent.c_str()); + fprintf(out, "%s android.util.SparseLongArray longMap = null;\n", + indent.c_str()); + fprintf(out, "%s android.util.SparseArray<String> stringMap = null;\n", + indent.c_str()); + fprintf(out, "%s android.util.SparseArray<Float> floatMap = null;\n", + indent.c_str()); + fprintf(out, "%s for (int i = 0; i < count; i++) {\n", indent.c_str()); + fprintf(out, "%s final int key = valueMap.keyAt(i);\n", + indent.c_str()); + fprintf(out, "%s final Object value = valueMap.valueAt(i);\n", + indent.c_str()); + fprintf(out, "%s if (value instanceof Integer) {\n", indent.c_str()); + fprintf(out, "%s if (null == intMap) {\n", indent.c_str()); + fprintf(out, + "%s intMap = new " + "android.util.SparseIntArray();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s intMap.put(key, (Integer) value);\n", + indent.c_str()); + fprintf(out, "%s } else if (value instanceof Long) {\n", + indent.c_str()); + fprintf(out, "%s if (null == longMap) {\n", indent.c_str()); + fprintf(out, + "%s longMap = new " + "android.util.SparseLongArray();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s longMap.put(key, (Long) value);\n", + indent.c_str()); + fprintf(out, "%s } else if (value instanceof String) {\n", + indent.c_str()); + fprintf(out, "%s if (null == stringMap) {\n", indent.c_str()); + fprintf(out, + "%s stringMap = new " + "android.util.SparseArray<>();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s stringMap.put(key, (String) value);\n", + indent.c_str()); + fprintf(out, "%s } else if (value instanceof Float) {\n", + indent.c_str()); + fprintf(out, "%s if (null == floatMap) {\n", indent.c_str()); + fprintf(out, + "%s floatMap = new " + "android.util.SparseArray<>();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s floatMap.put(key, (Float) value);\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, + "%s builder.writeKeyValuePairs(" + "intMap, longMap, stringMap, floatMap);\n", + indent.c_str()); + break; + default: + // Unsupported types: OBJECT, DOUBLE. + fprintf(stderr, "Encountered unsupported type."); + return 1; } + write_annotations(out, argIndex, fieldNumberToAtomDeclSet); argIndex++; } @@ -226,7 +261,7 @@ static int write_java_methods( fprintf(out, " QLogger.write(code"); argIndex = 1; for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + arg != signature.end(); arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { const char* uidName = attributionDecl.fields.front().name.c_str(); const char* tagName = attributionDecl.fields.back().name.c_str(); @@ -241,20 +276,18 @@ static int write_java_methods( argIndex++; } fprintf(out, ");\n"); - fprintf(out, " }\n"); // if + fprintf(out, " }\n"); // if } - fprintf(out, " }\n"); // method + fprintf(out, " }\n"); // method fprintf(out, "\n"); } return 0; - } -int write_stats_log_java(FILE* out, const Atoms& atoms, const AtomDecl &attributionDecl, - const string& moduleName, const string& javaClass, - const string& javaPackage, const bool supportQ, - const bool supportWorkSource) { +int write_stats_log_java(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& javaClass, const string& javaPackage, const bool supportQ, + const bool supportWorkSource) { // Print prelude fprintf(out, "// This file is autogenerated\n"); fprintf(out, "\n"); @@ -273,30 +306,25 @@ int write_stats_log_java(FILE* out, const Atoms& atoms, const AtomDecl &attribut fprintf(out, "\n"); fprintf(out, "/**\n"); fprintf(out, " * Utility class for logging statistics events.\n"); - if (DEFAULT_MODULE_NAME == moduleName) { - fprintf(out, " * @hide\n"); - } fprintf(out, " */\n"); fprintf(out, "public class %s {\n", javaClass.c_str()); - write_java_atom_codes(out, atoms, moduleName); - write_java_enum_values(out, atoms, moduleName); + write_java_atom_codes(out, atoms); + write_java_enum_values(out, atoms); + write_java_annotation_constants(out); int errors = 0; // Print write methods. fprintf(out, " // Write methods\n"); - errors += write_java_methods( - out, atoms.signatures_to_modules, attributionDecl, moduleName, supportQ); - errors += write_java_non_chained_methods( - out, atoms.non_chained_signatures_to_modules, moduleName); + errors += write_java_methods(out, atoms.signatureInfoMap, attributionDecl, supportQ); + errors += write_java_non_chained_methods(out, atoms.nonChainedSignatureInfoMap); if (supportWorkSource) { - errors += write_java_work_source_methods(out, atoms.signatures_to_modules, moduleName); + errors += write_java_work_source_methods(out, atoms.signatureInfoMap); } if (supportQ) { - errors += write_java_q_logger_class( - out, atoms.signatures_to_modules, attributionDecl, moduleName); + errors += write_java_q_logger_class(out, atoms.signatureInfoMap, attributionDecl); } fprintf(out, "}\n"); diff --git a/tools/stats_log_api_gen/java_writer.h b/tools/stats_log_api_gen/java_writer.h index 5b78f059c5b9..8b3b50588efc 100644 --- a/tools/stats_log_api_gen/java_writer.h +++ b/tools/stats_log_api_gen/java_writer.h @@ -16,25 +16,23 @@ #pragma once -#include "Collation.h" +#include <stdio.h> +#include <string.h> #include <map> #include <set> #include <vector> -#include <stdio.h> -#include <string.h> +#include "Collation.h" namespace android { namespace stats_log_api_gen { using namespace std; -int write_stats_log_java(FILE* out, const Atoms& atoms, const AtomDecl &attributionDecl, - const string& moduleName, const string& javaClass, - const string& javaPackage, const bool supportQ, +int write_stats_log_java(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& javaClass, const string& javaPackage, const bool supportQ, const bool supportWorkSource); } // namespace stats_log_api_gen } // namespace android - diff --git a/tools/stats_log_api_gen/java_writer_q.cpp b/tools/stats_log_api_gen/java_writer_q.cpp index db766b2ade71..d21e2708b724 100644 --- a/tools/stats_log_api_gen/java_writer_q.cpp +++ b/tools/stats_log_api_gen/java_writer_q.cpp @@ -15,6 +15,7 @@ */ #include "java_writer_q.h" + #include "utils.h" namespace android { @@ -24,7 +25,8 @@ void write_java_q_logging_constants(FILE* out, const string& indent) { fprintf(out, "%s// Payload limits.\n", indent.c_str()); fprintf(out, "%sprivate static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068;\n", indent.c_str()); fprintf(out, - "%sprivate static final int MAX_EVENT_PAYLOAD = LOGGER_ENTRY_MAX_PAYLOAD - 4;\n", + "%sprivate static final int MAX_EVENT_PAYLOAD = " + "LOGGER_ENTRY_MAX_PAYLOAD - 4;\n", indent.c_str()); // Value types. Must match with EventLog.java and log.h. @@ -37,47 +39,38 @@ void write_java_q_logging_constants(FILE* out, const string& indent) { fprintf(out, "%sprivate static final byte FLOAT_TYPE = 4;\n", indent.c_str()); // Size of each value type. - // Booleans, ints, floats, and enums take 5 bytes, 1 for the type and 4 for the value. + // Booleans, ints, floats, and enums take 5 bytes, 1 for the type and 4 for + // the value. fprintf(out, "\n"); fprintf(out, "%s// Size of each value type.\n", indent.c_str()); fprintf(out, "%sprivate static final int INT_TYPE_SIZE = 5;\n", indent.c_str()); fprintf(out, "%sprivate static final int FLOAT_TYPE_SIZE = 5;\n", indent.c_str()); // Longs take 9 bytes, 1 for the type and 8 for the value. fprintf(out, "%sprivate static final int LONG_TYPE_SIZE = 9;\n", indent.c_str()); - // Strings take 5 metadata bytes: 1 byte is for the type, 4 are for the length. + // Strings take 5 metadata bytes: 1 byte is for the type, 4 are for the + // length. fprintf(out, "%sprivate static final int STRING_TYPE_OVERHEAD = 5;\n", indent.c_str()); fprintf(out, "%sprivate static final int LIST_TYPE_OVERHEAD = 2;\n", indent.c_str()); } -int write_java_methods_q_schema( - FILE* out, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl, - const string& moduleName, - const string& indent) { +int write_java_methods_q_schema(FILE* out, const SignatureInfoMap& signatureInfoMap, + const AtomDecl& attributionDecl, const string& indent) { int requiredHelpers = 0; - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - // Skip if this signature is not needed for the module. - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { - continue; - } - + for (auto signatureInfoMapIt = signatureInfoMap.begin(); + signatureInfoMapIt != signatureInfoMap.end(); signatureInfoMapIt++) { // Print method signature. - vector<java_type_t> signature = signature_to_modules_it->first; + vector<java_type_t> signature = signatureInfoMapIt->first; fprintf(out, "%spublic static void write(int code", indent.c_str()); int argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (auto chainField : attributionDecl.fields) { - fprintf(out, ", %s[] %s", - java_type_name(chainField.javaType), chainField.name.c_str()); + fprintf(out, ", %s[] %s", java_type_name(chainField.javaType), + chainField.name.c_str()); } } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - // Module logging does not yet support key value pair. - fprintf(stderr, "Module logging does not yet support key value pair.\n"); - continue; + fprintf(out, ", android.util.SparseArray<Object> valueMap"); } else { fprintf(out, ", %s arg%d", java_type_name(*arg), argIndex); } @@ -89,87 +82,174 @@ int write_java_methods_q_schema( fprintf(out, "%s // Initial overhead of the list, timestamp, and atom tag.\n", indent.c_str()); fprintf(out, - "%s int needed = LIST_TYPE_OVERHEAD + LONG_TYPE_SIZE + INT_TYPE_SIZE;\n", + "%s int needed = LIST_TYPE_OVERHEAD + LONG_TYPE_SIZE + " + "INT_TYPE_SIZE;\n", indent.c_str()); argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { switch (*arg) { - case JAVA_TYPE_BOOLEAN: - case JAVA_TYPE_INT: - case JAVA_TYPE_FLOAT: - case JAVA_TYPE_ENUM: - fprintf(out, "%s needed += INT_TYPE_SIZE;\n", indent.c_str()); - break; - case JAVA_TYPE_LONG: - // Longs take 9 bytes, 1 for the type and 8 for the value. - fprintf(out, "%s needed += LONG_TYPE_SIZE;\n", indent.c_str()); - break; - case JAVA_TYPE_STRING: - // Strings take 5 metadata bytes + length of byte encoded string. - fprintf(out, "%s if (arg%d == null) {\n", indent.c_str(), argIndex); - fprintf(out, "%s arg%d = \"\";\n", indent.c_str(), argIndex); - fprintf(out, "%s }\n", indent.c_str()); - fprintf(out, - "%s byte[] arg%dBytes = " - "arg%d.getBytes(java.nio.charset.StandardCharsets.UTF_8);\n", - indent.c_str(), argIndex, argIndex); - fprintf(out, "%s needed += STRING_TYPE_OVERHEAD + arg%dBytes.length;\n", - indent.c_str(), argIndex); - break; - case JAVA_TYPE_BYTE_ARRAY: - // Byte arrays take 5 metadata bytes + length of byte array. - fprintf(out, "%s if (arg%d == null) {\n", indent.c_str(), argIndex); - fprintf(out, "%s arg%d = new byte[0];\n", indent.c_str(), argIndex); - fprintf(out, "%s }\n", indent.c_str()); - fprintf(out, "%s needed += STRING_TYPE_OVERHEAD + arg%d.length;\n", - indent.c_str(), argIndex); - break; - case JAVA_TYPE_ATTRIBUTION_CHAIN: - { - const char* uidName = attributionDecl.fields.front().name.c_str(); - const char* tagName = attributionDecl.fields.back().name.c_str(); - // Null checks on the params. - fprintf(out, "%s if (%s == null) {\n", indent.c_str(), uidName); - fprintf(out, "%s %s = new %s[0];\n", indent.c_str(), uidName, - java_type_name(attributionDecl.fields.front().javaType)); - fprintf(out, "%s }\n", indent.c_str()); - fprintf(out, "%s if (%s == null) {\n", indent.c_str(), tagName); - fprintf(out, "%s %s = new %s[0];\n", indent.c_str(), tagName, - java_type_name(attributionDecl.fields.back().javaType)); - fprintf(out, "%s }\n", indent.c_str()); - - // First check that the lengths of the uid and tag arrays are the same. - fprintf(out, "%s if (%s.length != %s.length) {\n", - indent.c_str(), uidName, tagName); - fprintf(out, "%s return;\n", indent.c_str()); - fprintf(out, "%s }\n", indent.c_str()); - fprintf(out, "%s int attrSize = LIST_TYPE_OVERHEAD;\n", indent.c_str()); - fprintf(out, "%s for (int i = 0; i < %s.length; i++) {\n", - indent.c_str(), tagName); - fprintf(out, "%s String str%d = (%s[i] == null) ? \"\" : %s[i];\n", - indent.c_str(), argIndex, tagName, tagName); - fprintf(out, - "%s int str%dlen = " - "str%d.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;\n", - indent.c_str(), argIndex, argIndex); - fprintf(out, - "%s attrSize += " - "LIST_TYPE_OVERHEAD + INT_TYPE_SIZE + STRING_TYPE_OVERHEAD + str%dlen;\n", - indent.c_str(), argIndex); - fprintf(out, "%s }\n", indent.c_str()); - fprintf(out, "%s needed += attrSize;\n", indent.c_str()); - break; - } - default: - // Unsupported types: OBJECT, DOUBLE, KEY_VALUE_PAIR. - fprintf(stderr, "Module logging does not yet support key value pair.\n"); - return 1; + case JAVA_TYPE_BOOLEAN: + case JAVA_TYPE_INT: + case JAVA_TYPE_FLOAT: + case JAVA_TYPE_ENUM: + fprintf(out, "%s needed += INT_TYPE_SIZE;\n", indent.c_str()); + break; + case JAVA_TYPE_LONG: + // Longs take 9 bytes, 1 for the type and 8 for the value. + fprintf(out, "%s needed += LONG_TYPE_SIZE;\n", indent.c_str()); + break; + case JAVA_TYPE_STRING: + // Strings take 5 metadata bytes + length of byte encoded string. + fprintf(out, "%s if (arg%d == null) {\n", indent.c_str(), argIndex); + fprintf(out, "%s arg%d = \"\";\n", indent.c_str(), argIndex); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, + "%s byte[] arg%dBytes = " + "arg%d.getBytes(java.nio.charset.StandardCharsets.UTF_8);\n", + indent.c_str(), argIndex, argIndex); + fprintf(out, "%s needed += STRING_TYPE_OVERHEAD + arg%dBytes.length;\n", + indent.c_str(), argIndex); + break; + case JAVA_TYPE_BYTE_ARRAY: + // Byte arrays take 5 metadata bytes + length of byte array. + fprintf(out, "%s if (arg%d == null) {\n", indent.c_str(), argIndex); + fprintf(out, "%s arg%d = new byte[0];\n", indent.c_str(), argIndex); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s needed += STRING_TYPE_OVERHEAD + arg%d.length;\n", + indent.c_str(), argIndex); + break; + case JAVA_TYPE_ATTRIBUTION_CHAIN: { + const char* uidName = attributionDecl.fields.front().name.c_str(); + const char* tagName = attributionDecl.fields.back().name.c_str(); + // Null checks on the params. + fprintf(out, "%s if (%s == null) {\n", indent.c_str(), uidName); + fprintf(out, "%s %s = new %s[0];\n", indent.c_str(), uidName, + java_type_name(attributionDecl.fields.front().javaType)); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s if (%s == null) {\n", indent.c_str(), tagName); + fprintf(out, "%s %s = new %s[0];\n", indent.c_str(), tagName, + java_type_name(attributionDecl.fields.back().javaType)); + fprintf(out, "%s }\n", indent.c_str()); + + // First check that the lengths of the uid and tag arrays are the + // same. + fprintf(out, "%s if (%s.length != %s.length) {\n", indent.c_str(), uidName, + tagName); + fprintf(out, "%s return;\n", indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s int attrSize = LIST_TYPE_OVERHEAD;\n", indent.c_str()); + fprintf(out, "%s for (int i = 0; i < %s.length; i++) {\n", indent.c_str(), + tagName); + fprintf(out, "%s String str%d = (%s[i] == null) ? \"\" : %s[i];\n", + indent.c_str(), argIndex, tagName, tagName); + fprintf(out, + "%s int str%dlen = " + "str%d.getBytes(java.nio.charset.StandardCharsets.UTF_8)." + "length;\n", + indent.c_str(), argIndex, argIndex); + fprintf(out, + "%s attrSize += " + "LIST_TYPE_OVERHEAD + INT_TYPE_SIZE + STRING_TYPE_OVERHEAD + " + "str%dlen;\n", + indent.c_str(), argIndex); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s needed += attrSize;\n", indent.c_str()); + break; + } + case JAVA_TYPE_KEY_VALUE_PAIR: { + fprintf(out, "%s // Calculate bytes needed by Key Value Pairs.\n", + indent.c_str()); + fprintf(out, "%s final int count = valueMap.size();\n", indent.c_str()); + fprintf(out, "%s android.util.SparseIntArray intMap = null;\n", + indent.c_str()); + fprintf(out, "%s android.util.SparseLongArray longMap = null;\n", + indent.c_str()); + fprintf(out, "%s android.util.SparseArray<String> stringMap = null;\n", + indent.c_str()); + fprintf(out, "%s android.util.SparseArray<Float> floatMap = null;\n", + indent.c_str()); + fprintf(out, "%s int keyValuePairSize = LIST_TYPE_OVERHEAD;\n", + indent.c_str()); + fprintf(out, "%s for (int i = 0; i < count; i++) {\n", indent.c_str()); + fprintf(out, "%s final int key = valueMap.keyAt(i);\n", indent.c_str()); + fprintf(out, "%s final Object value = valueMap.valueAt(i);\n", + indent.c_str()); + fprintf(out, "%s if (value instanceof Integer) {\n", indent.c_str()); + fprintf(out, "%s keyValuePairSize += LIST_TYPE_OVERHEAD\n", + indent.c_str()); + fprintf(out, "%s + INT_TYPE_SIZE + INT_TYPE_SIZE;\n", + indent.c_str()); + fprintf(out, "%s if (null == intMap) {\n", indent.c_str()); + fprintf(out, "%s intMap = new android.util.SparseIntArray();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s intMap.put(key, (Integer) value);\n", + indent.c_str()); + fprintf(out, "%s } else if (value instanceof Long) {\n", indent.c_str()); + fprintf(out, "%s keyValuePairSize += LIST_TYPE_OVERHEAD\n", + indent.c_str()); + fprintf(out, "%s + INT_TYPE_SIZE + LONG_TYPE_SIZE;\n", + indent.c_str()); + fprintf(out, "%s if (null == longMap) {\n", indent.c_str()); + fprintf(out, + "%s longMap = new " + "android.util.SparseLongArray();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s longMap.put(key, (Long) value);\n", indent.c_str()); + fprintf(out, "%s } else if (value instanceof String) {\n", + indent.c_str()); + fprintf(out, + "%s final String str = (value == null) ? \"\" : " + "(String) value;\n", + indent.c_str()); + fprintf(out, + "%s final int len = " + "str.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;\n", + indent.c_str()); + fprintf(out, + "%s keyValuePairSize += LIST_TYPE_OVERHEAD + " + "INT_TYPE_SIZE\n", + indent.c_str()); + fprintf(out, "%s + STRING_TYPE_OVERHEAD + len;\n", + indent.c_str()); + fprintf(out, "%s if (null == stringMap) {\n", indent.c_str()); + fprintf(out, + "%s stringMap = new " + "android.util.SparseArray<>();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s stringMap.put(key, str);\n", indent.c_str()); + fprintf(out, "%s } else if (value instanceof Float) {\n", + indent.c_str()); + fprintf(out, "%s keyValuePairSize += LIST_TYPE_OVERHEAD\n", + indent.c_str()); + fprintf(out, "%s + INT_TYPE_SIZE + FLOAT_TYPE_SIZE;\n", + indent.c_str()); + fprintf(out, "%s if (null == floatMap) {\n", indent.c_str()); + fprintf(out, + "%s floatMap = new " + "android.util.SparseArray<>();\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s floatMap.put(key, (Float) value);\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s needed += keyValuePairSize;\n", indent.c_str()); + break; + } + default: + // Unsupported types: OBJECT, DOUBLE. + fprintf(stderr, "Module logging does not yet support Object and Double.\n"); + return 1; } argIndex++; } - // Now we have the size that is needed. Check for overflow and return if needed. + // Now we have the size that is needed. Check for overflow and return if + // needed. fprintf(out, "%s if (needed > MAX_EVENT_PAYLOAD) {\n", indent.c_str()); fprintf(out, "%s return;\n", indent.c_str()); fprintf(out, "%s }\n", indent.c_str()); @@ -184,7 +264,8 @@ int write_java_methods_q_schema( fprintf(out, "%s pos += LIST_TYPE_OVERHEAD;\n", indent.c_str()); // Write timestamp. - fprintf(out, "%s long elapsedRealtime = SystemClock.elapsedRealtimeNanos();\n", indent.c_str()); + fprintf(out, "%s long elapsedRealtime = SystemClock.elapsedRealtimeNanos();\n", + indent.c_str()); fprintf(out, "%s buff[pos] = LONG_TYPE;\n", indent.c_str()); fprintf(out, "%s copyLong(buff, pos + 1, elapsedRealtime);\n", indent.c_str()); fprintf(out, "%s pos += LONG_TYPE_SIZE;\n", indent.c_str()); @@ -196,68 +277,82 @@ int write_java_methods_q_schema( // Write the args. argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { switch (*arg) { - case JAVA_TYPE_BOOLEAN: - fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); - fprintf(out, "%s copyInt(buff, pos + 1, arg%d? 1 : 0);\n", - indent.c_str(), argIndex); - fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); - break; - case JAVA_TYPE_INT: - case JAVA_TYPE_ENUM: - fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); - fprintf(out, "%s copyInt(buff, pos + 1, arg%d);\n", indent.c_str(), argIndex); - fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); - break; - case JAVA_TYPE_FLOAT: - requiredHelpers |= JAVA_MODULE_REQUIRES_FLOAT; - fprintf(out, "%s buff[pos] = FLOAT_TYPE;\n", indent.c_str()); - fprintf(out, "%s copyFloat(buff, pos + 1, arg%d);\n", indent.c_str(), argIndex); - fprintf(out, "%s pos += FLOAT_TYPE_SIZE;\n", indent.c_str()); - break; - case JAVA_TYPE_LONG: - fprintf(out, "%s buff[pos] = LONG_TYPE;\n", indent.c_str()); - fprintf(out, "%s copyLong(buff, pos + 1, arg%d);\n", indent.c_str(), argIndex); - fprintf(out, "%s pos += LONG_TYPE_SIZE;\n", indent.c_str()); - break; - case JAVA_TYPE_STRING: - fprintf(out, "%s buff[pos] = STRING_TYPE;\n", indent.c_str()); - fprintf(out, "%s copyInt(buff, pos + 1, arg%dBytes.length);\n", - indent.c_str(), argIndex); - fprintf(out, "%s System.arraycopy(" - "arg%dBytes, 0, buff, pos + STRING_TYPE_OVERHEAD, arg%dBytes.length);\n", - indent.c_str(), argIndex, argIndex); - fprintf(out, "%s pos += STRING_TYPE_OVERHEAD + arg%dBytes.length;\n", - indent.c_str(), argIndex); - break; - case JAVA_TYPE_BYTE_ARRAY: - fprintf(out, "%s buff[pos] = STRING_TYPE;\n", indent.c_str()); - fprintf(out, "%s copyInt(buff, pos + 1, arg%d.length);\n", - indent.c_str(), argIndex); - fprintf(out, "%s System.arraycopy(" - "arg%d, 0, buff, pos + STRING_TYPE_OVERHEAD, arg%d.length);\n", - indent.c_str(), argIndex, argIndex); - fprintf(out, "%s pos += STRING_TYPE_OVERHEAD + arg%d.length;\n", - indent.c_str(), argIndex); - break; - case JAVA_TYPE_ATTRIBUTION_CHAIN: - { - requiredHelpers |= JAVA_MODULE_REQUIRES_ATTRIBUTION; - const char* uidName = attributionDecl.fields.front().name.c_str(); - const char* tagName = attributionDecl.fields.back().name.c_str(); - - fprintf(out, "%s writeAttributionChain(buff, pos, %s, %s);\n", indent.c_str(), - uidName, tagName); - fprintf(out, "%s pos += attrSize;\n", indent.c_str()); - break; - } - default: - // Unsupported types: OBJECT, DOUBLE, KEY_VALUE_PAIR. - fprintf(stderr, - "Object, Double, and KeyValuePairs are not supported in module logging"); - return 1; + case JAVA_TYPE_BOOLEAN: + fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, arg%d? 1 : 0);\n", indent.c_str(), + argIndex); + fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); + break; + case JAVA_TYPE_INT: + case JAVA_TYPE_ENUM: + fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, arg%d);\n", indent.c_str(), + argIndex); + fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); + break; + case JAVA_TYPE_FLOAT: + requiredHelpers |= JAVA_MODULE_REQUIRES_FLOAT; + fprintf(out, "%s buff[pos] = FLOAT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyFloat(buff, pos + 1, arg%d);\n", indent.c_str(), + argIndex); + fprintf(out, "%s pos += FLOAT_TYPE_SIZE;\n", indent.c_str()); + break; + case JAVA_TYPE_LONG: + fprintf(out, "%s buff[pos] = LONG_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyLong(buff, pos + 1, arg%d);\n", indent.c_str(), + argIndex); + fprintf(out, "%s pos += LONG_TYPE_SIZE;\n", indent.c_str()); + break; + case JAVA_TYPE_STRING: + fprintf(out, "%s buff[pos] = STRING_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, arg%dBytes.length);\n", + indent.c_str(), argIndex); + fprintf(out, + "%s System.arraycopy(" + "arg%dBytes, 0, buff, pos + STRING_TYPE_OVERHEAD, " + "arg%dBytes.length);\n", + indent.c_str(), argIndex, argIndex); + fprintf(out, "%s pos += STRING_TYPE_OVERHEAD + arg%dBytes.length;\n", + indent.c_str(), argIndex); + break; + case JAVA_TYPE_BYTE_ARRAY: + fprintf(out, "%s buff[pos] = STRING_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, arg%d.length);\n", indent.c_str(), + argIndex); + fprintf(out, + "%s System.arraycopy(" + "arg%d, 0, buff, pos + STRING_TYPE_OVERHEAD, arg%d.length);\n", + indent.c_str(), argIndex, argIndex); + fprintf(out, "%s pos += STRING_TYPE_OVERHEAD + arg%d.length;\n", + indent.c_str(), argIndex); + break; + case JAVA_TYPE_ATTRIBUTION_CHAIN: { + requiredHelpers |= JAVA_MODULE_REQUIRES_ATTRIBUTION; + const char* uidName = attributionDecl.fields.front().name.c_str(); + const char* tagName = attributionDecl.fields.back().name.c_str(); + + fprintf(out, "%s writeAttributionChain(buff, pos, %s, %s);\n", + indent.c_str(), uidName, tagName); + fprintf(out, "%s pos += attrSize;\n", indent.c_str()); + break; + } + case JAVA_TYPE_KEY_VALUE_PAIR: + requiredHelpers |= JAVA_MODULE_REQUIRES_FLOAT; + requiredHelpers |= JAVA_MODULE_REQUIRES_KEY_VALUE_PAIRS; + fprintf(out, + "%s writeKeyValuePairs(buff, pos, (byte) count, intMap, " + "longMap, " + "stringMap, floatMap);\n", + indent.c_str()); + fprintf(out, "%s pos += keyValuePairSize;\n", indent.c_str()); + break; + default: + // Unsupported types: OBJECT, DOUBLE. + fprintf(stderr, "Object and Double are not supported in module logging"); + return 1; } argIndex++; } @@ -272,11 +367,8 @@ int write_java_methods_q_schema( return 0; } -void write_java_helpers_for_q_schema_methods( - FILE* out, - const AtomDecl &attributionDecl, - const int requiredHelpers, - const string& indent) { +void write_java_helpers_for_q_schema_methods(FILE* out, const AtomDecl& attributionDecl, + const int requiredHelpers, const string& indent) { fprintf(out, "\n"); fprintf(out, "%s// Helper methods for copying primitives\n", indent.c_str()); fprintf(out, "%sprivate static void copyInt(byte[] buff, int pos, int val) {\n", @@ -316,8 +408,7 @@ void write_java_helpers_for_q_schema_methods( fprintf(out, "%sprivate static void writeAttributionChain(byte[] buff, int pos", indent.c_str()); for (auto chainField : attributionDecl.fields) { - fprintf(out, ", %s[] %s", - java_type_name(chainField.javaType), chainField.name.c_str()); + fprintf(out, ", %s[] %s", java_type_name(chainField.javaType), chainField.name.c_str()); } fprintf(out, ") {\n"); @@ -333,8 +424,8 @@ void write_java_helpers_for_q_schema_methods( fprintf(out, "%s for (int i = 0; i < %s.length; i++) {\n", indent.c_str(), tagName); // Write the list begin. fprintf(out, "%s buff[pos] = LIST_TYPE;\n", indent.c_str()); - fprintf(out, "%s buff[pos + 1] = %lu;\n", - indent.c_str(), attributionDecl.fields.size()); + fprintf(out, "%s buff[pos + 1] = %lu;\n", indent.c_str(), + attributionDecl.fields.size()); fprintf(out, "%s pos += LIST_TYPE_OVERHEAD;\n", indent.c_str()); // Write the uid. @@ -343,30 +434,133 @@ void write_java_helpers_for_q_schema_methods( fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); // Write the tag. - fprintf(out, "%s String %sStr = (%s[i] == null) ? \"\" : %s[i];\n", - indent.c_str(), tagName, tagName, tagName); - fprintf(out, "%s byte[] %sByte = " + fprintf(out, "%s String %sStr = (%s[i] == null) ? \"\" : %s[i];\n", indent.c_str(), + tagName, tagName, tagName); + fprintf(out, + "%s byte[] %sByte = " "%sStr.getBytes(java.nio.charset.StandardCharsets.UTF_8);\n", indent.c_str(), tagName, tagName); fprintf(out, "%s buff[pos] = STRING_TYPE;\n", indent.c_str()); fprintf(out, "%s copyInt(buff, pos + 1, %sByte.length);\n", indent.c_str(), tagName); - fprintf(out, "%s System.arraycopy(" + fprintf(out, + "%s System.arraycopy(" "%sByte, 0, buff, pos + STRING_TYPE_OVERHEAD, %sByte.length);\n", indent.c_str(), tagName, tagName); - fprintf(out, "%s pos += STRING_TYPE_OVERHEAD + %sByte.length;\n", - indent.c_str(), tagName); + fprintf(out, "%s pos += STRING_TYPE_OVERHEAD + %sByte.length;\n", indent.c_str(), + tagName); + fprintf(out, "%s }\n", indent.c_str()); + fprintf(out, "%s}\n", indent.c_str()); + fprintf(out, "\n"); + } + + if (requiredHelpers & JAVA_MODULE_REQUIRES_KEY_VALUE_PAIRS) { + fprintf(out, + "%sprivate static void writeKeyValuePairs(byte[] buff, int pos, " + "byte numPairs,\n", + indent.c_str()); + fprintf(out, "%s final android.util.SparseIntArray intMap,\n", indent.c_str()); + fprintf(out, "%s final android.util.SparseLongArray longMap,\n", indent.c_str()); + fprintf(out, "%s final android.util.SparseArray<String> stringMap,\n", + indent.c_str()); + fprintf(out, "%s final android.util.SparseArray<Float> floatMap) {\n", + indent.c_str()); + + // Start list of lists. + fprintf(out, "%s buff[pos] = LIST_TYPE;\n", indent.c_str()); + fprintf(out, "%s buff[pos + 1] = (byte) numPairs;\n", indent.c_str()); + fprintf(out, "%s pos += LIST_TYPE_OVERHEAD;\n", indent.c_str()); + + // Write integers. + fprintf(out, "%s final int intMapSize = null == intMap ? 0 : intMap.size();\n", + indent.c_str()); + fprintf(out, "%s for (int i = 0; i < intMapSize; i++) {\n", indent.c_str()); + fprintf(out, "%s buff[pos] = LIST_TYPE;\n", indent.c_str()); + fprintf(out, "%s buff[pos + 1] = (byte) 2;\n", indent.c_str()); + fprintf(out, "%s pos += LIST_TYPE_OVERHEAD;\n", indent.c_str()); + fprintf(out, "%s final int key = intMap.keyAt(i);\n", indent.c_str()); + fprintf(out, "%s final int value = intMap.valueAt(i);\n", indent.c_str()); + fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, key);\n", indent.c_str()); + fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); + fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, value);\n", indent.c_str()); + fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + + // Write longs. + fprintf(out, "%s final int longMapSize = null == longMap ? 0 : longMap.size();\n", + indent.c_str()); + fprintf(out, "%s for (int i = 0; i < longMapSize; i++) {\n", indent.c_str()); + fprintf(out, "%s buff[pos] = LIST_TYPE;\n", indent.c_str()); + fprintf(out, "%s buff[pos + 1] = (byte) 2;\n", indent.c_str()); + fprintf(out, "%s pos += LIST_TYPE_OVERHEAD;\n", indent.c_str()); + fprintf(out, "%s final int key = longMap.keyAt(i);\n", indent.c_str()); + fprintf(out, "%s final long value = longMap.valueAt(i);\n", indent.c_str()); + fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, key);\n", indent.c_str()); + fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); + fprintf(out, "%s buff[pos] = LONG_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyLong(buff, pos + 1, value);\n", indent.c_str()); + fprintf(out, "%s pos += LONG_TYPE_SIZE;\n", indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + + // Write Strings. + fprintf(out, + "%s final int stringMapSize = null == stringMap ? 0 : " + "stringMap.size();\n", + indent.c_str()); + fprintf(out, "%s for (int i = 0; i < stringMapSize; i++) {\n", indent.c_str()); + fprintf(out, "%s buff[pos] = LIST_TYPE;\n", indent.c_str()); + fprintf(out, "%s buff[pos + 1] = (byte) 2;\n", indent.c_str()); + fprintf(out, "%s pos += LIST_TYPE_OVERHEAD;\n", indent.c_str()); + fprintf(out, "%s final int key = stringMap.keyAt(i);\n", indent.c_str()); + fprintf(out, "%s final String value = stringMap.valueAt(i);\n", indent.c_str()); + fprintf(out, + "%s final byte[] valueBytes = " + "value.getBytes(java.nio.charset.StandardCharsets.UTF_8);\n", + indent.c_str()); + fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, key);\n", indent.c_str()); + fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); + fprintf(out, "%s buff[pos] = STRING_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, valueBytes.length);\n", indent.c_str()); + fprintf(out, + "%s System.arraycopy(" + "valueBytes, 0, buff, pos + STRING_TYPE_OVERHEAD, " + "valueBytes.length);\n", + indent.c_str()); + fprintf(out, "%s pos += STRING_TYPE_OVERHEAD + valueBytes.length;\n", + indent.c_str()); + fprintf(out, "%s }\n", indent.c_str()); + + // Write floats. + fprintf(out, + "%s final int floatMapSize = null == floatMap ? 0 : " + "floatMap.size();\n", + indent.c_str()); + fprintf(out, "%s for (int i = 0; i < floatMapSize; i++) {\n", indent.c_str()); + fprintf(out, "%s buff[pos] = LIST_TYPE;\n", indent.c_str()); + fprintf(out, "%s buff[pos + 1] = (byte) 2;\n", indent.c_str()); + fprintf(out, "%s pos += LIST_TYPE_OVERHEAD;\n", indent.c_str()); + fprintf(out, "%s final int key = floatMap.keyAt(i);\n", indent.c_str()); + fprintf(out, "%s final float value = floatMap.valueAt(i);\n", indent.c_str()); + fprintf(out, "%s buff[pos] = INT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyInt(buff, pos + 1, key);\n", indent.c_str()); + fprintf(out, "%s pos += INT_TYPE_SIZE;\n", indent.c_str()); + fprintf(out, "%s buff[pos] = FLOAT_TYPE;\n", indent.c_str()); + fprintf(out, "%s copyFloat(buff, pos + 1, value);\n", indent.c_str()); + fprintf(out, "%s pos += FLOAT_TYPE_SIZE;\n", indent.c_str()); fprintf(out, "%s }\n", indent.c_str()); fprintf(out, "%s}\n", indent.c_str()); fprintf(out, "\n"); } } -// This method is called in main.cpp to generate StatsLog for modules that's compatible with -// Q at compile-time. +// This method is called in main.cpp to generate StatsLog for modules that's +// compatible with Q at compile-time. int write_stats_log_java_q_for_module(FILE* out, const Atoms& atoms, - const AtomDecl &attributionDecl, const string& moduleName, - const string& javaClass, const string& javaPackage, - const bool supportWorkSource) { + const AtomDecl& attributionDecl, const string& javaClass, + const string& javaPackage, const bool supportWorkSource) { // Print prelude fprintf(out, "// This file is autogenerated\n"); fprintf(out, "\n"); @@ -385,19 +579,17 @@ int write_stats_log_java_q_for_module(FILE* out, const Atoms& atoms, write_java_q_logging_constants(out, " "); - write_java_atom_codes(out, atoms, moduleName); + write_java_atom_codes(out, atoms); - write_java_enum_values(out, atoms, moduleName); + write_java_enum_values(out, atoms); int errors = 0; // Print write methods fprintf(out, " // Write methods\n"); - errors += write_java_methods_q_schema(out, atoms.signatures_to_modules, attributionDecl, - moduleName, " "); - errors += write_java_non_chained_methods(out, atoms.non_chained_signatures_to_modules, - moduleName); + errors += write_java_methods_q_schema(out, atoms.signatureInfoMap, attributionDecl, " "); + errors += write_java_non_chained_methods(out, atoms.nonChainedSignatureInfoMap); if (supportWorkSource) { - errors += write_java_work_source_methods(out, atoms.signatures_to_modules, moduleName); + errors += write_java_work_source_methods(out, atoms.signatureInfoMap); } fprintf(out, "}\n"); @@ -405,69 +597,5 @@ int write_stats_log_java_q_for_module(FILE* out, const Atoms& atoms, return errors; } -#if defined(STATS_SCHEMA_LEGACY) -static void write_java_method( - FILE* out, - const string& method_name, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl) { - - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - vector<java_type_t> signature = signature_to_modules_it->first; - fprintf(out, " /** @hide */\n"); - fprintf(out, " public static native int %s(int code", method_name.c_str()); - int argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (auto chainField : attributionDecl.fields) { - fprintf(out, ", %s[] %s", - java_type_name(chainField.javaType), chainField.name.c_str()); - } - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, ", android.util.SparseArray<Object> value_map"); - } else { - fprintf(out, ", %s arg%d", java_type_name(*arg), argIndex); - } - argIndex++; - } - fprintf(out, ");\n"); - fprintf(out, "\n"); - } -} - -int write_stats_log_java_q(FILE* out, const Atoms& atoms, const AtomDecl &attributionDecl, - const bool supportWorkSource) { - // Print prelude - fprintf(out, "// This file is autogenerated\n"); - fprintf(out, "\n"); - fprintf(out, "package android.util;\n"); - fprintf(out, "\n"); - fprintf(out, "\n"); - fprintf(out, "/**\n"); - fprintf(out, " * API For logging statistics events.\n"); - fprintf(out, " * @hide\n"); - fprintf(out, " */\n"); - fprintf(out, "public class StatsLogInternal {\n"); - write_java_atom_codes(out, atoms, DEFAULT_MODULE_NAME); - - write_java_enum_values(out, atoms, DEFAULT_MODULE_NAME); - - // Print write methods - fprintf(out, " // Write methods\n"); - write_java_method(out, "write", atoms.signatures_to_modules, attributionDecl); - write_java_method(out, "write_non_chained", atoms.non_chained_signatures_to_modules, - attributionDecl); - if (supportWorkSource) { - write_java_work_source_methods(out, atoms.signatures_to_modules, DEFAULT_MODULE_NAME); - } - - fprintf(out, "}\n"); - - return 0; -} -#endif - } // namespace stats_log_api_gen } // namespace android diff --git a/tools/stats_log_api_gen/java_writer_q.h b/tools/stats_log_api_gen/java_writer_q.h index 7d734df1e118..c511a8436416 100644 --- a/tools/stats_log_api_gen/java_writer_q.h +++ b/tools/stats_log_api_gen/java_writer_q.h @@ -16,14 +16,14 @@ #pragma once -#include "Collation.h" +#include <stdio.h> +#include <string.h> #include <map> #include <set> #include <vector> -#include <stdio.h> -#include <string.h> +#include "Collation.h" namespace android { namespace stats_log_api_gen { @@ -32,26 +32,15 @@ using namespace std; void write_java_q_logging_constants(FILE* out, const string& indent); -int write_java_methods_q_schema( - FILE* out, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl, - const string& moduleName, - const string& indent); +int write_java_methods_q_schema(FILE* out, const SignatureInfoMap& signatureInfoMap, + const AtomDecl& attributionDecl, const string& indent); -void write_java_helpers_for_q_schema_methods( - FILE * out, - const AtomDecl &attributionDecl, - const int requiredHelpers, - const string& indent); +void write_java_helpers_for_q_schema_methods(FILE* out, const AtomDecl& attributionDecl, + const int requiredHelpers, const string& indent); int write_stats_log_java_q_for_module(FILE* out, const Atoms& atoms, - const AtomDecl &attributionDecl, const string& moduleName, const string& javaClass, - const string& javaPackage, const bool supportWorkSource); + const AtomDecl& attributionDecl, const string& javaClass, + const string& javaPackage, const bool supportWorkSource); -#if defined(STATS_SCHEMA_LEGACY) -int write_stats_log_java_q(FILE* out, const Atoms& atoms, const AtomDecl &attributionDecl, - const bool supportWorkSource); -#endif } // namespace stats_log_api_gen } // namespace android diff --git a/tools/stats_log_api_gen/main.cpp b/tools/stats_log_api_gen/main.cpp index ddbf22c7f12a..b888ce904b31 100644 --- a/tools/stats_log_api_gen/main.cpp +++ b/tools/stats_log_api_gen/main.cpp @@ -1,25 +1,20 @@ +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <map> +#include <set> +#include <vector> #include "Collation.h" -#include "atoms_info_writer.h" -#if !defined(STATS_SCHEMA_LEGACY) +#include "frameworks/base/cmds/statsd/src/atoms.pb.h" #include "java_writer.h" -#endif #include "java_writer_q.h" #include "native_writer.h" #include "utils.h" -#include "frameworks/base/cmds/statsd/src/atoms.pb.h" - -#include <map> -#include <set> -#include <vector> - -#include <getopt.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - using namespace google::protobuf; using namespace std; @@ -28,514 +23,52 @@ namespace stats_log_api_gen { using android::os::statsd::Atom; -// Hide the JNI write helpers that are not used in the new schema. -// TODO(b/145100015): Remove this and other JNI related functionality once StatsEvent migration is -// complete. -#if defined(STATS_SCHEMA_LEGACY) -// JNI helpers. -static const char* -jni_type_name(java_type_t type) -{ - switch (type) { - case JAVA_TYPE_BOOLEAN: - return "jboolean"; - case JAVA_TYPE_INT: - case JAVA_TYPE_ENUM: - return "jint"; - case JAVA_TYPE_LONG: - return "jlong"; - case JAVA_TYPE_FLOAT: - return "jfloat"; - case JAVA_TYPE_DOUBLE: - return "jdouble"; - case JAVA_TYPE_STRING: - return "jstring"; - case JAVA_TYPE_BYTE_ARRAY: - return "jbyteArray"; - default: - return "UNKNOWN"; - } -} - -static const char* -jni_array_type_name(java_type_t type) -{ - switch (type) { - case JAVA_TYPE_INT: - return "jintArray"; - case JAVA_TYPE_FLOAT: - return "jfloatArray"; - case JAVA_TYPE_STRING: - return "jobjectArray"; - default: - return "UNKNOWN"; - } -} - -static string -jni_function_name(const string& method_name, const vector<java_type_t>& signature) -{ - string result("StatsLog_" + method_name); - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - switch (*arg) { - case JAVA_TYPE_BOOLEAN: - result += "_boolean"; - break; - case JAVA_TYPE_INT: - case JAVA_TYPE_ENUM: - result += "_int"; - break; - case JAVA_TYPE_LONG: - result += "_long"; - break; - case JAVA_TYPE_FLOAT: - result += "_float"; - break; - case JAVA_TYPE_DOUBLE: - result += "_double"; - break; - case JAVA_TYPE_STRING: - result += "_String"; - break; - case JAVA_TYPE_ATTRIBUTION_CHAIN: - result += "_AttributionChain"; - break; - case JAVA_TYPE_KEY_VALUE_PAIR: - result += "_KeyValuePairs"; - break; - case JAVA_TYPE_BYTE_ARRAY: - result += "_bytes"; - break; - default: - result += "_UNKNOWN"; - break; - } - } - return result; -} - -static const char* -java_type_signature(java_type_t type) -{ - switch (type) { - case JAVA_TYPE_BOOLEAN: - return "Z"; - case JAVA_TYPE_INT: - case JAVA_TYPE_ENUM: - return "I"; - case JAVA_TYPE_LONG: - return "J"; - case JAVA_TYPE_FLOAT: - return "F"; - case JAVA_TYPE_DOUBLE: - return "D"; - case JAVA_TYPE_STRING: - return "Ljava/lang/String;"; - case JAVA_TYPE_BYTE_ARRAY: - return "[B"; - default: - return "UNKNOWN"; - } -} - -static string -jni_function_signature(const vector<java_type_t>& signature, const AtomDecl &attributionDecl) -{ - string result("(I"); - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (auto chainField : attributionDecl.fields) { - result += "["; - result += java_type_signature(chainField.javaType); - } - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - result += "Landroid/util/SparseArray;"; - } else { - result += java_type_signature(*arg); - } - } - result += ")I"; - return result; -} - -static void write_key_value_map_jni(FILE* out) { - fprintf(out, " std::map<int, int32_t> int32_t_map;\n"); - fprintf(out, " std::map<int, int64_t> int64_t_map;\n"); - fprintf(out, " std::map<int, float> float_map;\n"); - fprintf(out, " std::map<int, char const*> string_map;\n\n"); - - fprintf(out, " jclass jmap_class = env->FindClass(\"android/util/SparseArray\");\n"); - - fprintf(out, " jmethodID jget_size_method = env->GetMethodID(jmap_class, \"size\", \"()I\");\n"); - fprintf(out, " jmethodID jget_key_method = env->GetMethodID(jmap_class, \"keyAt\", \"(I)I\");\n"); - fprintf(out, " jmethodID jget_value_method = env->GetMethodID(jmap_class, \"valueAt\", \"(I)Ljava/lang/Object;\");\n\n"); - - - fprintf(out, " std::vector<std::unique_ptr<ScopedUtfChars>> scoped_ufs;\n\n"); - - fprintf(out, " jclass jint_class = env->FindClass(\"java/lang/Integer\");\n"); - fprintf(out, " jclass jlong_class = env->FindClass(\"java/lang/Long\");\n"); - fprintf(out, " jclass jfloat_class = env->FindClass(\"java/lang/Float\");\n"); - fprintf(out, " jclass jstring_class = env->FindClass(\"java/lang/String\");\n"); - fprintf(out, " jmethodID jget_int_method = env->GetMethodID(jint_class, \"intValue\", \"()I\");\n"); - fprintf(out, " jmethodID jget_long_method = env->GetMethodID(jlong_class, \"longValue\", \"()J\");\n"); - fprintf(out, " jmethodID jget_float_method = env->GetMethodID(jfloat_class, \"floatValue\", \"()F\");\n\n"); - - fprintf(out, " jint jsize = env->CallIntMethod(value_map, jget_size_method);\n"); - fprintf(out, " for(int i = 0; i < jsize; i++) {\n"); - fprintf(out, " jint key = env->CallIntMethod(value_map, jget_key_method, i);\n"); - fprintf(out, " jobject jvalue_obj = env->CallObjectMethod(value_map, jget_value_method, i);\n"); - fprintf(out, " if (jvalue_obj == NULL) { continue; }\n"); - fprintf(out, " if (env->IsInstanceOf(jvalue_obj, jint_class)) {\n"); - fprintf(out, " int32_t_map[key] = env->CallIntMethod(jvalue_obj, jget_int_method);\n"); - fprintf(out, " } else if (env->IsInstanceOf(jvalue_obj, jlong_class)) {\n"); - fprintf(out, " int64_t_map[key] = env->CallLongMethod(jvalue_obj, jget_long_method);\n"); - fprintf(out, " } else if (env->IsInstanceOf(jvalue_obj, jfloat_class)) {\n"); - fprintf(out, " float_map[key] = env->CallFloatMethod(jvalue_obj, jget_float_method);\n"); - fprintf(out, " } else if (env->IsInstanceOf(jvalue_obj, jstring_class)) {\n"); - fprintf(out, " std::unique_ptr<ScopedUtfChars> utf(new ScopedUtfChars(env, (jstring)jvalue_obj));\n"); - fprintf(out, " if (utf->c_str() != NULL) { string_map[key] = utf->c_str(); }\n"); - fprintf(out, " scoped_ufs.push_back(std::move(utf));\n"); - fprintf(out, " }\n"); - fprintf(out, " }\n"); -} - -static int -write_stats_log_jni_method(FILE* out, const string& java_method_name, const string& cpp_method_name, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl) { - // Print write methods - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - vector<java_type_t> signature = signature_to_modules_it->first; - int argIndex; - - fprintf(out, "static int\n"); - fprintf(out, "%s(JNIEnv* env, jobject clazz UNUSED, jint code", - jni_function_name(java_method_name, signature).c_str()); - argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (auto chainField : attributionDecl.fields) { - fprintf(out, ", %s %s", jni_array_type_name(chainField.javaType), - chainField.name.c_str()); - } - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, ", jobject value_map"); - } else { - fprintf(out, ", %s arg%d", jni_type_name(*arg), argIndex); - } - argIndex++; - } - fprintf(out, ")\n"); - - fprintf(out, "{\n"); - - // Prepare strings - argIndex = 1; - bool hadStringOrChain = false; - bool isKeyValuePairAtom = false; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_STRING) { - hadStringOrChain = true; - fprintf(out, " const char* str%d;\n", argIndex); - fprintf(out, " if (arg%d != NULL) {\n", argIndex); - fprintf(out, " str%d = env->GetStringUTFChars(arg%d, NULL);\n", - argIndex, argIndex); - fprintf(out, " } else {\n"); - fprintf(out, " str%d = NULL;\n", argIndex); - fprintf(out, " }\n"); - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - hadStringOrChain = true; - fprintf(out, " jbyte* jbyte_array%d;\n", argIndex); - fprintf(out, " const char* str%d;\n", argIndex); - fprintf(out, " int str%d_length = 0;\n", argIndex); - fprintf(out, - " if (arg%d != NULL && env->GetArrayLength(arg%d) > " - "0) {\n", - argIndex, argIndex); - fprintf(out, - " jbyte_array%d = " - "env->GetByteArrayElements(arg%d, NULL);\n", - argIndex, argIndex); - fprintf(out, - " str%d_length = env->GetArrayLength(arg%d);\n", - argIndex, argIndex); - fprintf(out, - " str%d = " - "reinterpret_cast<char*>(env->GetByteArrayElements(arg%" - "d, NULL));\n", - argIndex, argIndex); - fprintf(out, " } else {\n"); - fprintf(out, " jbyte_array%d = NULL;\n", argIndex); - fprintf(out, " str%d = NULL;\n", argIndex); - fprintf(out, " }\n"); - - fprintf(out, - " android::util::BytesField bytesField%d(str%d, " - "str%d_length);", - argIndex, argIndex, argIndex); - - } else if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - hadStringOrChain = true; - for (auto chainField : attributionDecl.fields) { - fprintf(out, " size_t %s_length = env->GetArrayLength(%s);\n", - chainField.name.c_str(), chainField.name.c_str()); - if (chainField.name != attributionDecl.fields.front().name) { - fprintf(out, " if (%s_length != %s_length) {\n", - chainField.name.c_str(), - attributionDecl.fields.front().name.c_str()); - fprintf(out, " return -EINVAL;\n"); - fprintf(out, " }\n"); - } - if (chainField.javaType == JAVA_TYPE_INT) { - fprintf(out, " jint* %s_array = env->GetIntArrayElements(%s, NULL);\n", - chainField.name.c_str(), chainField.name.c_str()); - } else if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, " std::vector<%s> %s_vec;\n", - cpp_type_name(chainField.javaType), chainField.name.c_str()); - fprintf(out, " std::vector<ScopedUtfChars*> scoped_%s_vec;\n", - chainField.name.c_str()); - fprintf(out, " for (size_t i = 0; i < %s_length; ++i) {\n", - chainField.name.c_str()); - fprintf(out, " jstring jstr = " - "(jstring)env->GetObjectArrayElement(%s, i);\n", - chainField.name.c_str()); - fprintf(out, " if (jstr == NULL) {\n"); - fprintf(out, " %s_vec.push_back(NULL);\n", - chainField.name.c_str()); - fprintf(out, " } else {\n"); - fprintf(out, " ScopedUtfChars* scoped_%s = " - "new ScopedUtfChars(env, jstr);\n", - chainField.name.c_str()); - fprintf(out, " %s_vec.push_back(scoped_%s->c_str());\n", - chainField.name.c_str(), chainField.name.c_str()); - fprintf(out, " scoped_%s_vec.push_back(scoped_%s);\n", - chainField.name.c_str(), chainField.name.c_str()); - fprintf(out, " }\n"); - fprintf(out, " }\n"); - } - fprintf(out, "\n"); - } - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - isKeyValuePairAtom = true; - } - argIndex++; - } - // Emit this to quiet the unused parameter warning if there were no strings or attribution - // chains. - if (!hadStringOrChain && !isKeyValuePairAtom) { - fprintf(out, " (void)env;\n"); - } - if (isKeyValuePairAtom) { - write_key_value_map_jni(out); - } - - // stats_write call - argIndex = 1; - fprintf(out, "\n int ret = android::util::%s(code", - cpp_method_name.c_str()); - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (auto chainField : attributionDecl.fields) { - if (chainField.javaType == JAVA_TYPE_INT) { - fprintf(out, ", (const %s*)%s_array, %s_length", - cpp_type_name(chainField.javaType), - chainField.name.c_str(), chainField.name.c_str()); - } else if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, ", %s_vec", chainField.name.c_str()); - } - } - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, ", int32_t_map, int64_t_map, string_map, float_map"); - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, ", bytesField%d", argIndex); - } else { - const char* argName = - (*arg == JAVA_TYPE_STRING) ? "str" : "arg"; - fprintf(out, ", (%s)%s%d", cpp_type_name(*arg), argName, argIndex); - } - argIndex++; - } - fprintf(out, ");\n"); - fprintf(out, "\n"); - - // Clean up strings - argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_STRING) { - fprintf(out, " if (str%d != NULL) {\n", argIndex); - fprintf(out, " env->ReleaseStringUTFChars(arg%d, str%d);\n", - argIndex, argIndex); - fprintf(out, " }\n"); - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, " if (str%d != NULL) { \n", argIndex); - fprintf(out, - " env->ReleaseByteArrayElements(arg%d, " - "jbyte_array%d, 0);\n", - argIndex, argIndex); - fprintf(out, " }\n"); - } else if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (auto chainField : attributionDecl.fields) { - if (chainField.javaType == JAVA_TYPE_INT) { - fprintf(out, " env->ReleaseIntArrayElements(%s, %s_array, 0);\n", - chainField.name.c_str(), chainField.name.c_str()); - } else if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, " for (size_t i = 0; i < scoped_%s_vec.size(); ++i) {\n", - chainField.name.c_str()); - fprintf(out, " delete scoped_%s_vec[i];\n", chainField.name.c_str()); - fprintf(out, " }\n"); - } - } - } - argIndex++; - } - - fprintf(out, " return ret;\n"); - - fprintf(out, "}\n"); - fprintf(out, "\n"); - } - - - return 0; -} - -void write_jni_registration(FILE* out, const string& java_method_name, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl) { - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - vector<java_type_t> signature = signature_to_modules_it->first; - fprintf(out, " { \"%s\", \"%s\", (void*)%s },\n", - java_method_name.c_str(), - jni_function_signature(signature, attributionDecl).c_str(), - jni_function_name(java_method_name, signature).c_str()); - } -} -#endif // JNI helpers. - -static int -#if defined(STATS_SCHEMA_LEGACY) -write_stats_log_jni(FILE* out, const Atoms& atoms, const AtomDecl &attributionDecl) -#else -// Write empty JNI file that doesn't contain any JNI methods. -// TODO(b/145100015): remove this function and all JNI autogen code once StatsEvent migration is -// complete. -write_stats_log_jni(FILE* out) -#endif -{ - // Print prelude - fprintf(out, "// This file is autogenerated\n"); - fprintf(out, "\n"); - -#if defined(STATS_SCHEMA_LEGACY) - fprintf(out, "#include <statslog.h>\n"); - fprintf(out, "\n"); - fprintf(out, "#include <nativehelper/JNIHelp.h>\n"); - fprintf(out, "#include <nativehelper/ScopedUtfChars.h>\n"); - fprintf(out, "#include <utils/Vector.h>\n"); -#endif - fprintf(out, "#include \"core_jni_helpers.h\"\n"); - fprintf(out, "#include \"jni.h\"\n"); - fprintf(out, "\n"); -#if defined(STATS_SCHEMA_LEGACY) - fprintf(out, "#define UNUSED __attribute__((__unused__))\n"); - fprintf(out, "\n"); -#endif - - fprintf(out, "namespace android {\n"); - fprintf(out, "\n"); - -#if defined(STATS_SCHEMA_LEGACY) - write_stats_log_jni_method(out, "write", "stats_write", atoms.signatures_to_modules, attributionDecl); - write_stats_log_jni_method(out, "write_non_chained", "stats_write_non_chained", - atoms.non_chained_signatures_to_modules, attributionDecl); -#endif - - // Print registration function table - fprintf(out, "/*\n"); - fprintf(out, " * JNI registration.\n"); - fprintf(out, " */\n"); - fprintf(out, "static const JNINativeMethod gRegisterMethods[] = {\n"); -#if defined(STATS_SCHEMA_LEGACY) - write_jni_registration(out, "write", atoms.signatures_to_modules, attributionDecl); - write_jni_registration(out, "write_non_chained", atoms.non_chained_signatures_to_modules, - attributionDecl); -#endif - fprintf(out, "};\n"); - fprintf(out, "\n"); - - // Print registration function - fprintf(out, "int register_android_util_StatsLogInternal(JNIEnv* env) {\n"); - fprintf(out, " return RegisterMethodsOrDie(\n"); - fprintf(out, " env,\n"); - fprintf(out, " \"android/util/StatsLogInternal\",\n"); - fprintf(out, " gRegisterMethods, NELEM(gRegisterMethods));\n"); - fprintf(out, "}\n"); - - fprintf(out, "\n"); - fprintf(out, "} // namespace android\n"); - return 0; -} - -static void -print_usage() -{ +static void print_usage() { fprintf(stderr, "usage: stats-log-api-gen OPTIONS\n"); fprintf(stderr, "\n"); fprintf(stderr, "OPTIONS\n"); fprintf(stderr, " --cpp FILENAME the header file to output for write helpers\n"); fprintf(stderr, " --header FILENAME the cpp file to output for write helpers\n"); - fprintf(stderr, - " --atomsInfoCpp FILENAME the header file to output for statsd metadata\n"); - fprintf(stderr, " --atomsInfoHeader FILENAME the cpp file to output for statsd metadata\n"); fprintf(stderr, " --help this message\n"); fprintf(stderr, " --java FILENAME the java file to output\n"); - fprintf(stderr, " --jni FILENAME the jni file to output\n"); fprintf(stderr, " --module NAME optional, module name to generate outputs for\n"); - fprintf(stderr, " --namespace COMMA,SEP,NAMESPACE required for cpp/header with module\n"); - fprintf(stderr, " comma separated namespace of the files\n"); - fprintf(stderr," --importHeader NAME required for cpp/jni to say which header to import " + fprintf(stderr, + " --namespace COMMA,SEP,NAMESPACE required for cpp/header with " + "module\n"); + fprintf(stderr, + " comma separated namespace of " + "the files\n"); + fprintf(stderr, + " --importHeader NAME required for cpp/jni to say which header to " + "import " "for write helpers\n"); - fprintf(stderr," --atomsInfoImportHeader NAME required for cpp to say which header to import " - "for statsd metadata\n"); fprintf(stderr, " --javaPackage PACKAGE the package for the java file.\n"); fprintf(stderr, " required for java with module\n"); fprintf(stderr, " --javaClass CLASS the class name of the java class.\n"); fprintf(stderr, " Optional for Java with module.\n"); fprintf(stderr, " Default is \"StatsLogInternal\"\n"); fprintf(stderr, " --supportQ Include runtime support for Android Q.\n"); - fprintf(stderr, " --worksource Include support for logging WorkSource objects.\n"); - fprintf(stderr, " --compileQ Include compile-time support for Android Q " + fprintf(stderr, + " --worksource Include support for logging WorkSource " + "objects.\n"); + fprintf(stderr, + " --compileQ Include compile-time support for Android Q " "(Java only).\n"); } /** * Do the argument parsing and execute the tasks. */ -static int -run(int argc, char const*const* argv) -{ +static int run(int argc, char const* const* argv) { string cppFilename; string headerFilename; string javaFilename; - string jniFilename; - string atomsInfoCppFilename; - string atomsInfoHeaderFilename; + string javaPackage; + string javaClass; string moduleName = DEFAULT_MODULE_NAME; string cppNamespace = DEFAULT_CPP_NAMESPACE; string cppHeaderImport = DEFAULT_CPP_HEADER_IMPORT; - string atomsInfoCppHeaderImport = DEFAULT_ATOMS_INFO_CPP_HEADER_IMPORT; - string javaPackage = DEFAULT_JAVA_PACKAGE; - string javaClass = DEFAULT_JAVA_CLASS; bool supportQ = false; bool supportWorkSource = false; bool compileQ = false; @@ -566,13 +99,6 @@ run(int argc, char const*const* argv) return 1; } javaFilename = argv[index]; - } else if (0 == strcmp("--jni", argv[index])) { - index++; - if (index >= argc) { - print_usage(); - return 1; - } - jniFilename = argv[index]; } else if (0 == strcmp("--module", argv[index])) { index++; if (index >= argc) { @@ -608,27 +134,6 @@ run(int argc, char const*const* argv) return 1; } javaClass = argv[index]; - } else if (0 == strcmp("--atomsInfoHeader", argv[index])) { - index++; - if (index >= argc) { - print_usage(); - return 1; - } - atomsInfoHeaderFilename = argv[index]; - } else if (0 == strcmp("--atomsInfoCpp", argv[index])) { - index++; - if (index >= argc) { - print_usage(); - return 1; - } - atomsInfoCppFilename = argv[index]; - } else if (0 == strcmp("--atomsInfoImportHeader", argv[index])) { - index++; - if (index >= argc) { - print_usage(); - return 1; - } - atomsInfoCppHeaderImport = argv[index]; } else if (0 == strcmp("--supportQ", argv[index])) { supportQ = true; } else if (0 == strcmp("--worksource", argv[index])) { @@ -640,12 +145,7 @@ run(int argc, char const*const* argv) index++; } - if (cppFilename.size() == 0 - && headerFilename.size() == 0 - && javaFilename.size() == 0 - && jniFilename.size() == 0 - && atomsInfoHeaderFilename.size() == 0 - && atomsInfoCppFilename.size() == 0) { + if (cppFilename.size() == 0 && headerFilename.size() == 0 && javaFilename.size() == 0) { print_usage(); return 1; } @@ -664,39 +164,15 @@ run(int argc, char const*const* argv) // Collate the parameters Atoms atoms; - int errorCount = collate_atoms(Atom::descriptor(), &atoms); + int errorCount = collate_atoms(Atom::descriptor(), moduleName, &atoms); if (errorCount != 0) { return 1; } AtomDecl attributionDecl; vector<java_type_t> attributionSignature; - collate_atom(android::os::statsd::AttributionNode::descriptor(), - &attributionDecl, &attributionSignature); - - // Write the atoms info .cpp file - if (atomsInfoCppFilename.size() != 0) { - FILE* out = fopen(atomsInfoCppFilename.c_str(), "w"); - if (out == NULL) { - fprintf(stderr, "Unable to open file for write: %s\n", atomsInfoCppFilename.c_str()); - return 1; - } - errorCount = android::stats_log_api_gen::write_atoms_info_cpp( - out, atoms, cppNamespace, atomsInfoCppHeaderImport, cppHeaderImport); - fclose(out); - } - - // Write the atoms info .h file - if (atomsInfoHeaderFilename.size() != 0) { - FILE* out = fopen(atomsInfoHeaderFilename.c_str(), "w"); - if (out == NULL) { - fprintf(stderr, "Unable to open file for write: %s\n", atomsInfoHeaderFilename.c_str()); - return 1; - } - errorCount = android::stats_log_api_gen::write_atoms_info_header(out, atoms, cppNamespace); - fclose(out); - } - + collate_atom(android::os::statsd::AttributionNode::descriptor(), &attributionDecl, + &attributionSignature); // Write the .cpp file if (cppFilename.size() != 0) { @@ -710,13 +186,14 @@ run(int argc, char const*const* argv) fprintf(stderr, "Must supply --namespace if supplying a specific module\n"); return 1; } - // If this is for a specific module, the header file to import must also be provided. + // If this is for a specific module, the header file to import must also be + // provided. if (moduleName != DEFAULT_MODULE_NAME && cppHeaderImport == DEFAULT_CPP_HEADER_IMPORT) { fprintf(stderr, "Must supply --headerImport if supplying a specific module\n"); return 1; } errorCount = android::stats_log_api_gen::write_stats_log_cpp( - out, atoms, attributionDecl, moduleName, cppNamespace, cppHeaderImport, supportQ); + out, atoms, attributionDecl, cppNamespace, cppHeaderImport, supportQ); fclose(out); } @@ -731,62 +208,42 @@ run(int argc, char const*const* argv) if (moduleName != DEFAULT_MODULE_NAME && cppNamespace == DEFAULT_CPP_NAMESPACE) { fprintf(stderr, "Must supply --namespace if supplying a specific module\n"); } - errorCount = android::stats_log_api_gen::write_stats_log_header( - out, atoms, attributionDecl, moduleName, cppNamespace); + errorCount = android::stats_log_api_gen::write_stats_log_header(out, atoms, attributionDecl, + cppNamespace); fclose(out); } // Write the .java file if (javaFilename.size() != 0) { - FILE* out = fopen(javaFilename.c_str(), "w"); - if (out == NULL) { - fprintf(stderr, "Unable to open file for write: %s\n", javaFilename.c_str()); + if (javaClass.size() == 0) { + fprintf(stderr, "Must supply --javaClass if supplying a Java filename"); return 1; } -#if defined(STATS_SCHEMA_LEGACY) - if (moduleName == DEFAULT_MODULE_NAME) { - errorCount = android::stats_log_api_gen::write_stats_log_java_q( - out, atoms, attributionDecl, supportWorkSource); - } else { - errorCount = android::stats_log_api_gen::write_stats_log_java_q_for_module( - out, atoms, attributionDecl, moduleName, javaClass, javaPackage, - supportWorkSource); + if (javaPackage.size() == 0) { + fprintf(stderr, "Must supply --javaPackage if supplying a Java filename"); + return 1; + } + if (moduleName.size() == 0) { + fprintf(stderr, "Must supply --module if supplying a Java filename"); + return 1; } -#else - if (moduleName == DEFAULT_MODULE_NAME) { - javaClass = "StatsLogInternal"; - javaPackage = "android.util"; + + FILE* out = fopen(javaFilename.c_str(), "w"); + if (out == NULL) { + fprintf(stderr, "Unable to open file for write: %s\n", javaFilename.c_str()); + return 1; } + if (compileQ) { errorCount = android::stats_log_api_gen::write_stats_log_java_q_for_module( - out, atoms, attributionDecl, moduleName, javaClass, javaPackage, - supportWorkSource); + out, atoms, attributionDecl, javaClass, javaPackage, supportWorkSource); } else { errorCount = android::stats_log_api_gen::write_stats_log_java( - out, atoms, attributionDecl, moduleName, javaClass, javaPackage, supportQ, + out, atoms, attributionDecl, javaClass, javaPackage, supportQ, supportWorkSource); } -#endif - - fclose(out); - } - - // Write the jni file - if (jniFilename.size() != 0) { - FILE* out = fopen(jniFilename.c_str(), "w"); - if (out == NULL) { - fprintf(stderr, "Unable to open file for write: %s\n", jniFilename.c_str()); - return 1; - } - -#if defined(STATS_SCHEMA_LEGACY) - errorCount = android::stats_log_api_gen::write_stats_log_jni( - out, atoms, attributionDecl); -#else - errorCount = android::stats_log_api_gen::write_stats_log_jni(out); -#endif fclose(out); } @@ -800,9 +257,7 @@ run(int argc, char const*const* argv) /** * Main. */ -int -main(int argc, char const*const* argv) -{ +int main(int argc, char const* const* argv) { GOOGLE_PROTOBUF_VERIFY_VERSION; return android::stats_log_api_gen::run(argc, argv); diff --git a/tools/stats_log_api_gen/native_writer.cpp b/tools/stats_log_api_gen/native_writer.cpp index c7a34feff94b..0c6c0099e459 100644 --- a/tools/stats_log_api_gen/native_writer.cpp +++ b/tools/stats_log_api_gen/native_writer.cpp @@ -15,42 +15,93 @@ */ #include "native_writer.h" -#include "native_writer_q.h" + #include "utils.h" namespace android { namespace stats_log_api_gen { -#if !defined(STATS_SCHEMA_LEGACY) -static void write_native_key_value_pairs_for_type(FILE* out, const int argIndex, - const int typeIndex, const string& type, const string& valueFieldName) { - fprintf(out, " for (const auto& it : arg%d_%d) {\n", argIndex, typeIndex); - fprintf(out, " pairs.push_back(" - "{ .key = it.first, .valueType = %s, .%s = it.second });\n", - type.c_str(), valueFieldName.c_str()); - fprintf(out, " }\n"); +static void write_native_annotation_constants(FILE* out) { + fprintf(out, "// Annotation constants.\n"); + for (const auto& [id, name] : ANNOTATION_ID_CONSTANTS) { + fprintf(out, "const uint8_t %s = %hhu;\n", name.c_str(), id); + } + fprintf(out, "\n"); +} + +static void write_annotations(FILE* out, int argIndex, + const FieldNumberToAtomDeclSet& fieldNumberToAtomDeclSet, + const string& methodPrefix, const string& methodSuffix) { + FieldNumberToAtomDeclSet::const_iterator fieldNumberToAtomDeclSetIt = + fieldNumberToAtomDeclSet.find(argIndex); + if (fieldNumberToAtomDeclSet.end() == fieldNumberToAtomDeclSetIt) { + return; + } + const AtomDeclSet& atomDeclSet = fieldNumberToAtomDeclSetIt->second; + for (const shared_ptr<AtomDecl>& atomDecl : atomDeclSet) { + const string atomConstant = make_constant_name(atomDecl->name); + fprintf(out, " if (%s == code) {\n", atomConstant.c_str()); + const AnnotationSet& annotations = atomDecl->fieldNumberToAnnotations.at(argIndex); + int resetState = -1; + int defaultState = -1; + for (const shared_ptr<Annotation>& annotation : annotations) { + const string& annotationConstant = ANNOTATION_ID_CONSTANTS.at(annotation->annotationId); + switch (annotation->type) { + case ANNOTATION_TYPE_INT: + if (ANNOTATION_ID_TRIGGER_STATE_RESET == annotation->annotationId) { + resetState = annotation->value.intValue; + } else if (ANNOTATION_ID_DEFAULT_STATE == annotation->annotationId) { + defaultState = annotation->value.intValue; + } else { + fprintf(out, " %saddInt32Annotation(%s%s, %d);\n", + methodPrefix.c_str(), methodSuffix.c_str(), + annotationConstant.c_str(), annotation->value.intValue); + } + break; + case ANNOTATION_TYPE_BOOL: + // TODO(b/151786433): Write annotation constant name instead of + // annotation id literal. + fprintf(out, " %saddBoolAnnotation(%s%s, %s);\n", methodPrefix.c_str(), + methodSuffix.c_str(), annotationConstant.c_str(), + annotation->value.boolValue ? "true" : "false"); + break; + default: + break; + } + } + if (defaultState != -1 && resetState != -1) { + const string& annotationConstant = + ANNOTATION_ID_CONSTANTS.at(ANNOTATION_ID_TRIGGER_STATE_RESET); + fprintf(out, " if (arg%d == %d) {\n", argIndex, resetState); + fprintf(out, " %saddInt32Annotation(%s%s, %d);\n", methodPrefix.c_str(), + methodSuffix.c_str(), annotationConstant.c_str(), defaultState); + fprintf(out, " }\n"); + } + fprintf(out, " }\n"); + } } static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName, const bool supportQ) { + const AtomDecl& attributionDecl, const bool supportQ) { fprintf(out, "\n"); - for (auto signature_to_modules_it = atoms.signatures_to_modules.begin(); - signature_to_modules_it != atoms.signatures_to_modules.end(); signature_to_modules_it++) { - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { + for (auto signatureInfoMapIt = atoms.signatureInfoMap.begin(); + signatureInfoMapIt != atoms.signatureInfoMap.end(); signatureInfoMapIt++) { + vector<java_type_t> signature = signatureInfoMapIt->first; + const FieldNumberToAtomDeclSet& fieldNumberToAtomDeclSet = signatureInfoMapIt->second; + // Key value pairs not supported in native. + if (find(signature.begin(), signature.end(), JAVA_TYPE_KEY_VALUE_PAIR) != signature.end()) { continue; } - vector<java_type_t> signature = signature_to_modules_it->first; - - write_native_method_signature(out, "int stats_write", signature, - attributionDecl, " {"); + write_native_method_signature(out, "int stats_write", signature, attributionDecl, " {"); int argIndex = 1; if (supportQ) { fprintf(out, " StatsEventCompat event;\n"); fprintf(out, " event.setAtomId(code);\n"); + write_annotations(out, ATOM_ID_FIELD_NUMBER, fieldNumberToAtomDeclSet, "event.", ""); for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + arg != signature.end(); arg++) { switch (*arg) { case JAVA_TYPE_ATTRIBUTION_CHAIN: { const char* uidName = attributionDecl.fields.front().name.c_str(); @@ -59,11 +110,6 @@ static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, uidName, uidName, tagName); break; } - case JAVA_TYPE_KEY_VALUE_PAIR: - fprintf(out, " event.writeKeyValuePairs(" - "arg%d_1, arg%d_2, arg%d_3, arg%d_4);\n", - argIndex, argIndex, argIndex, argIndex); - break; case JAVA_TYPE_BYTE_ARRAY: fprintf(out, " event.writeByteArray(arg%d.arg, arg%d.arg_length);\n", argIndex, argIndex); @@ -71,7 +117,7 @@ static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, case JAVA_TYPE_BOOLEAN: fprintf(out, " event.writeBool(arg%d);\n", argIndex); break; - case JAVA_TYPE_INT: // Fall through. + case JAVA_TYPE_INT: // Fall through. case JAVA_TYPE_ENUM: fprintf(out, " event.writeInt32(arg%d);\n", argIndex); break; @@ -85,74 +131,66 @@ static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, fprintf(out, " event.writeString(arg%d);\n", argIndex); break; default: - // Unsupported types: OBJECT, DOUBLE. + // Unsupported types: OBJECT, DOUBLE, KEY_VALUE_PAIRS. fprintf(stderr, "Encountered unsupported type."); return 1; } + write_annotations(out, argIndex, fieldNumberToAtomDeclSet, "event.", ""); argIndex++; } fprintf(out, " return event.writeToSocket();\n"); } else { - fprintf(out, " struct stats_event* event = stats_event_obtain();\n"); - fprintf(out, " stats_event_set_atom_id(event, code);\n"); + fprintf(out, " AStatsEvent* event = AStatsEvent_obtain();\n"); + fprintf(out, " AStatsEvent_setAtomId(event, code);\n"); + write_annotations(out, ATOM_ID_FIELD_NUMBER, fieldNumberToAtomDeclSet, "AStatsEvent_", + "event, "); for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + arg != signature.end(); arg++) { switch (*arg) { case JAVA_TYPE_ATTRIBUTION_CHAIN: { const char* uidName = attributionDecl.fields.front().name.c_str(); const char* tagName = attributionDecl.fields.back().name.c_str(); fprintf(out, - " stats_event_write_attribution_chain(event, " + " AStatsEvent_writeAttributionChain(event, " "reinterpret_cast<const uint32_t*>(%s), %s.data(), " "static_cast<uint8_t>(%s_length));\n", uidName, tagName, uidName); break; } - case JAVA_TYPE_KEY_VALUE_PAIR: - fprintf(out, " std::vector<key_value_pair> pairs;\n"); - write_native_key_value_pairs_for_type( - out, argIndex, 1, "INT32_TYPE", "int32Value"); - write_native_key_value_pairs_for_type( - out, argIndex, 2, "INT64_TYPE", "int64Value"); - write_native_key_value_pairs_for_type( - out, argIndex, 3, "STRING_TYPE", "stringValue"); - write_native_key_value_pairs_for_type( - out, argIndex, 4, "FLOAT_TYPE", "floatValue"); - fprintf(out, - " stats_event_write_key_value_pairs(event, pairs.data(), " - "static_cast<uint8_t>(pairs.size()));\n"); - break; case JAVA_TYPE_BYTE_ARRAY: fprintf(out, - " stats_event_write_byte_array(event, " - "reinterpret_cast<const uint8_t*>(arg%d.arg), arg%d.arg_length);\n", + " AStatsEvent_writeByteArray(event, " + "reinterpret_cast<const uint8_t*>(arg%d.arg), " + "arg%d.arg_length);\n", argIndex, argIndex); break; case JAVA_TYPE_BOOLEAN: - fprintf(out, " stats_event_write_bool(event, arg%d);\n", argIndex); + fprintf(out, " AStatsEvent_writeBool(event, arg%d);\n", argIndex); break; - case JAVA_TYPE_INT: // Fall through. + case JAVA_TYPE_INT: // Fall through. case JAVA_TYPE_ENUM: - fprintf(out, " stats_event_write_int32(event, arg%d);\n", argIndex); + fprintf(out, " AStatsEvent_writeInt32(event, arg%d);\n", argIndex); break; case JAVA_TYPE_FLOAT: - fprintf(out, " stats_event_write_float(event, arg%d);\n", argIndex); + fprintf(out, " AStatsEvent_writeFloat(event, arg%d);\n", argIndex); break; case JAVA_TYPE_LONG: - fprintf(out, " stats_event_write_int64(event, arg%d);\n", argIndex); + fprintf(out, " AStatsEvent_writeInt64(event, arg%d);\n", argIndex); break; case JAVA_TYPE_STRING: - fprintf(out, " stats_event_write_string8(event, arg%d);\n", argIndex); + fprintf(out, " AStatsEvent_writeString(event, arg%d);\n", argIndex); break; default: - // Unsupported types: OBJECT, DOUBLE. + // Unsupported types: OBJECT, DOUBLE, KEY_VALUE_PAIRS fprintf(stderr, "Encountered unsupported type."); return 1; } + write_annotations(out, argIndex, fieldNumberToAtomDeclSet, "AStatsEvent_", + "event, "); argIndex++; } - fprintf(out, " const int ret = stats_event_write(event);\n"); - fprintf(out, " stats_event_release(event);\n"); + fprintf(out, " const int ret = AStatsEvent_write(event);\n"); + fprintf(out, " AStatsEvent_release(event);\n"); fprintf(out, " return ret;\n"); } fprintf(out, "}\n\n"); @@ -161,17 +199,18 @@ static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, } static void write_native_stats_write_non_chained_methods(FILE* out, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName) { + const AtomDecl& attributionDecl) { fprintf(out, "\n"); - for (auto signature_it = atoms.non_chained_signatures_to_modules.begin(); - signature_it != atoms.non_chained_signatures_to_modules.end(); signature_it++) { - if (!signature_needed_for_module(signature_it->second, moduleName)) { + for (auto signature_it = atoms.nonChainedSignatureInfoMap.begin(); + signature_it != atoms.nonChainedSignatureInfoMap.end(); signature_it++) { + vector<java_type_t> signature = signature_it->first; + // Key value pairs not supported in native. + if (find(signature.begin(), signature.end(), JAVA_TYPE_KEY_VALUE_PAIR) != signature.end()) { continue; } - vector<java_type_t> signature = signature_it->first; write_native_method_signature(out, "int stats_write_non_chained", signature, - attributionDecl, " {"); + attributionDecl, " {"); vector<java_type_t> newSignature; @@ -194,63 +233,42 @@ static void write_native_stats_write_non_chained_methods(FILE* out, const Atoms& fprintf(out, "}\n\n"); } - } -#endif - -static void write_native_method_header( - FILE* out, - const string& methodName, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const AtomDecl &attributionDecl, const string& moduleName) { - - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - // Skip if this signature is not needed for the module. - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { + +static void write_native_method_header(FILE* out, const string& methodName, + const SignatureInfoMap& signatureInfoMap, + const AtomDecl& attributionDecl) { + for (auto signatureInfoMapIt = signatureInfoMap.begin(); + signatureInfoMapIt != signatureInfoMap.end(); signatureInfoMapIt++) { + vector<java_type_t> signature = signatureInfoMapIt->first; + + // Key value pairs not supported in native. + if (find(signature.begin(), signature.end(), JAVA_TYPE_KEY_VALUE_PAIR) != signature.end()) { continue; } - - vector<java_type_t> signature = signature_to_modules_it->first; write_native_method_signature(out, methodName, signature, attributionDecl, ";"); } } -int write_stats_log_cpp(FILE *out, const Atoms &atoms, const AtomDecl &attributionDecl, - const string& moduleName, const string& cppNamespace, - const string& importHeader, const bool supportQ) { +int write_stats_log_cpp(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& cppNamespace, const string& importHeader, + const bool supportQ) { // Print prelude fprintf(out, "// This file is autogenerated\n"); fprintf(out, "\n"); fprintf(out, "#include <%s>\n", importHeader.c_str()); -#if defined(STATS_SCHEMA_LEGACY) - (void)supportQ; // Workaround for unused parameter error. - write_native_cpp_includes_q(out); -#else if (supportQ) { fprintf(out, "#include <StatsEventCompat.h>\n"); } else { fprintf(out, "#include <stats_event.h>\n"); } -#endif fprintf(out, "\n"); write_namespace(out, cppNamespace); -#if defined(STATS_SCHEMA_LEGACY) - write_native_stats_log_cpp_globals_q(out); - write_native_get_timestamp_ns_q(out); - write_native_try_stats_write_methods_q(out, atoms, attributionDecl, moduleName); - write_native_stats_write_methods_q(out, "int stats_write", atoms, attributionDecl, moduleName, - "try_stats_write"); - write_native_try_stats_write_non_chained_methods_q(out, atoms, attributionDecl, moduleName); - write_native_stats_write_non_chained_methods_q(out, "int stats_write_non_chained", atoms, - attributionDecl, moduleName, "try_stats_write_non_chained"); -#else - write_native_stats_write_methods(out, atoms, attributionDecl, moduleName, supportQ); - write_native_stats_write_non_chained_methods(out, atoms, attributionDecl, moduleName); -#endif + write_native_stats_write_methods(out, atoms, attributionDecl, supportQ); + write_native_stats_write_non_chained_methods(out, atoms, attributionDecl); // Print footer fprintf(out, "\n"); @@ -259,8 +277,8 @@ int write_stats_log_cpp(FILE *out, const Atoms &atoms, const AtomDecl &attributi return 0; } -int write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attributionDecl, - const string& moduleName, const string& cppNamespace) { +int write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& cppNamespace) { // Print prelude fprintf(out, "// This file is autogenerated\n"); fprintf(out, "\n"); @@ -279,37 +297,33 @@ int write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attrib fprintf(out, " */\n"); fprintf(out, "\n"); - write_native_atom_constants(out, atoms, attributionDecl, moduleName); + write_native_atom_constants(out, atoms, attributionDecl); // Print constants for the enum values. fprintf(out, "//\n"); fprintf(out, "// Constants for enum values\n"); fprintf(out, "//\n\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - // Skip if the atom is not needed for the module. - if (!atom_needed_for_module(*atom, moduleName)) { - continue; - } - - for (vector<AtomField>::const_iterator field = atom->fields.begin(); - field != atom->fields.end(); field++) { + for (AtomDeclSet::const_iterator atomIt = atoms.decls.begin(); atomIt != atoms.decls.end(); + atomIt++) { + for (vector<AtomField>::const_iterator field = (*atomIt)->fields.begin(); + field != (*atomIt)->fields.end(); field++) { if (field->javaType == JAVA_TYPE_ENUM) { - fprintf(out, "// Values for %s.%s\n", atom->message.c_str(), - field->name.c_str()); + fprintf(out, "// Values for %s.%s\n", (*atomIt)->message.c_str(), + field->name.c_str()); for (map<int, string>::const_iterator value = field->enumValues.begin(); - value != field->enumValues.end(); value++) { + value != field->enumValues.end(); value++) { fprintf(out, "const int32_t %s__%s__%s = %d;\n", - make_constant_name(atom->message).c_str(), - make_constant_name(field->name).c_str(), - make_constant_name(value->second).c_str(), - value->first); + make_constant_name((*atomIt)->message).c_str(), + make_constant_name(field->name).c_str(), + make_constant_name(value->second).c_str(), value->first); } fprintf(out, "\n"); } } } + write_native_annotation_constants(out); + fprintf(out, "struct BytesField {\n"); fprintf(out, " BytesField(char const* array, size_t len) : arg(array), " @@ -323,14 +337,13 @@ int write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attrib fprintf(out, "//\n"); fprintf(out, "// Write methods\n"); fprintf(out, "//\n"); - write_native_method_header(out, "int stats_write", atoms.signatures_to_modules, attributionDecl, - moduleName); + write_native_method_header(out, "int stats_write", atoms.signatureInfoMap, attributionDecl); fprintf(out, "//\n"); fprintf(out, "// Write flattened methods\n"); fprintf(out, "//\n"); - write_native_method_header(out, "int stats_write_non_chained", - atoms.non_chained_signatures_to_modules, attributionDecl, moduleName); + write_native_method_header(out, "int stats_write_non_chained", atoms.nonChainedSignatureInfoMap, + attributionDecl); fprintf(out, "\n"); write_closing_namespace(out, cppNamespace); diff --git a/tools/stats_log_api_gen/native_writer.h b/tools/stats_log_api_gen/native_writer.h index aafa96ece2d0..264d4db29fc9 100644 --- a/tools/stats_log_api_gen/native_writer.h +++ b/tools/stats_log_api_gen/native_writer.h @@ -16,22 +16,22 @@ #pragma once -#include "Collation.h" - #include <stdio.h> #include <string.h> +#include "Collation.h" + namespace android { namespace stats_log_api_gen { using namespace std; -int write_stats_log_cpp(FILE *out, const Atoms &atoms, const AtomDecl &attributionDecl, - const string& moduleName, const string& cppNamespace, const string& importHeader, - const bool supportQ); +int write_stats_log_cpp(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& cppNamespace, const string& importHeader, + const bool supportQ); -int write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl &attributionDecl, - const string& moduleName, const string& cppNamespace); +int write_stats_log_header(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, + const string& cppNamespace); } // namespace stats_log_api_gen } // namespace android diff --git a/tools/stats_log_api_gen/native_writer_q.cpp b/tools/stats_log_api_gen/native_writer_q.cpp deleted file mode 100644 index 299873dad975..000000000000 --- a/tools/stats_log_api_gen/native_writer_q.cpp +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2019, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "native_writer_q.h" -#include "utils.h" - -namespace android { -namespace stats_log_api_gen { - -static void write_native_stats_write_body_q(FILE* out, const vector<java_type_t>& signature, - const AtomDecl& attributionDecl, const string& indent, const string& tryMethodName) { - fprintf(out, "%sint ret = 0;\n", indent.c_str()); - - fprintf(out, "%sfor(int retry = 0; retry < 2; ++retry) {\n", indent.c_str()); - fprintf(out, "%s ret = ", indent.c_str()); - write_native_method_call(out, tryMethodName, signature, attributionDecl); - fprintf(out, "%s if (ret >= 0) { break; }\n", indent.c_str()); - - fprintf(out, "%s {\n", indent.c_str()); - fprintf(out, "%s std::lock_guard<std::mutex> lock(mLogdRetryMutex);\n", indent.c_str()); - fprintf(out, "%s if ((get_elapsed_realtime_ns() - lastRetryTimestampNs) <= " - "kMinRetryIntervalNs) break;\n", indent.c_str()); - fprintf(out, "%s lastRetryTimestampNs = get_elapsed_realtime_ns();\n", - indent.c_str()); - fprintf(out, "%s }\n", indent.c_str()); - fprintf(out, "%s std::this_thread::sleep_for(std::chrono::milliseconds(10));\n", - indent.c_str()); - fprintf(out, "%s}\n", indent.c_str()); - fprintf(out, "%sif (ret < 0) {\n", indent.c_str()); - fprintf(out, "%s note_log_drop(ret, code);\n", indent.c_str()); - fprintf(out, "%s}\n", indent.c_str()); - fprintf(out, "%sreturn ret;\n", indent.c_str()); -} - -void write_native_cpp_includes_q(FILE* out) { - fprintf(out, "#include <mutex>\n"); - fprintf(out, "#include <chrono>\n"); - fprintf(out, "#include <thread>\n"); - fprintf(out, "#ifdef __ANDROID__\n"); - fprintf(out, "#include <cutils/properties.h>\n"); - fprintf(out, "#endif\n"); - fprintf(out, "#include <stats_event_list.h>\n"); - fprintf(out, "#include <log/log.h>\n"); - fprintf(out, "#include <time.h>\n"); -} - -void write_native_get_timestamp_ns_q(FILE* out) { - fprintf(out, "\n"); - fprintf(out, "static int64_t get_elapsed_realtime_ns() {\n"); - fprintf(out, " struct timespec t;\n"); - fprintf(out, " t.tv_sec = t.tv_nsec = 0;\n"); - fprintf(out, " clock_gettime(CLOCK_BOOTTIME, &t);\n"); - fprintf(out, " return (int64_t)t.tv_sec * 1000000000LL + t.tv_nsec;\n"); - fprintf(out, "}\n"); -} - -void write_native_stats_log_cpp_globals_q(FILE* out) { - fprintf(out, "// the single event tag id for all stats logs\n"); - fprintf(out, "const static int kStatsEventTag = 1937006964;\n"); - fprintf(out, "#ifdef __ANDROID__\n"); - fprintf(out, - "const static bool kStatsdEnabled = property_get_bool(\"ro.statsd.enable\", true);\n"); - fprintf(out, "#else\n"); - fprintf(out, "const static bool kStatsdEnabled = false;\n"); - fprintf(out, "#endif\n"); - - fprintf(out, "int64_t lastRetryTimestampNs = -1;\n"); - fprintf(out, "const int64_t kMinRetryIntervalNs = NS_PER_SEC * 60 * 20; // 20 minutes\n"); - fprintf(out, "static std::mutex mLogdRetryMutex;\n"); -} - -void write_native_try_stats_write_methods_q(FILE* out, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName) { - fprintf(out, "\n"); - for (auto signature_to_modules_it = atoms.signatures_to_modules.begin(); - signature_to_modules_it != atoms.signatures_to_modules.end(); signature_to_modules_it++) { - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { - continue; - } - vector<java_type_t> signature = signature_to_modules_it->first; - - write_native_method_signature(out, "static int try_stats_write", signature, - attributionDecl, " {"); - - int argIndex = 1; - fprintf(out, " if (kStatsdEnabled) {\n"); - fprintf(out, " stats_event_list event(kStatsEventTag);\n"); - fprintf(out, " event << get_elapsed_realtime_ns();\n\n"); - fprintf(out, " event << code;\n\n"); - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (const auto &chainField : attributionDecl.fields) { - if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, " if (%s_length != %s.size()) {\n", - attributionDecl.fields.front().name.c_str(), chainField.name.c_str()); - fprintf(out, " return -EINVAL;\n"); - fprintf(out, " }\n"); - } - } - fprintf(out, "\n event.begin();\n"); - fprintf(out, " for (size_t i = 0; i < %s_length; ++i) {\n", - attributionDecl.fields.front().name.c_str()); - fprintf(out, " event.begin();\n"); - for (const auto &chainField : attributionDecl.fields) { - if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, " if (%s[i] != NULL) {\n", chainField.name.c_str()); - fprintf(out, " event << %s[i];\n", chainField.name.c_str()); - fprintf(out, " } else {\n"); - fprintf(out, " event << \"\";\n"); - fprintf(out, " }\n"); - } else { - fprintf(out, " event << %s[i];\n", chainField.name.c_str()); - } - } - fprintf(out, " event.end();\n"); - fprintf(out, " }\n"); - fprintf(out, " event.end();\n\n"); - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, " event.begin();\n\n"); - fprintf(out, " for (const auto& it : arg%d_1) {\n", argIndex); - fprintf(out, " event.begin();\n"); - fprintf(out, " event << it.first;\n"); - fprintf(out, " event << it.second;\n"); - fprintf(out, " event.end();\n"); - fprintf(out, " }\n"); - - fprintf(out, " for (const auto& it : arg%d_2) {\n", argIndex); - fprintf(out, " event.begin();\n"); - fprintf(out, " event << it.first;\n"); - fprintf(out, " event << it.second;\n"); - fprintf(out, " event.end();\n"); - fprintf(out, " }\n"); - - fprintf(out, " for (const auto& it : arg%d_3) {\n", argIndex); - fprintf(out, " event.begin();\n"); - fprintf(out, " event << it.first;\n"); - fprintf(out, " event << it.second;\n"); - fprintf(out, " event.end();\n"); - fprintf(out, " }\n"); - - fprintf(out, " for (const auto& it : arg%d_4) {\n", argIndex); - fprintf(out, " event.begin();\n"); - fprintf(out, " event << it.first;\n"); - fprintf(out, " event << it.second;\n"); - fprintf(out, " event.end();\n"); - fprintf(out, " }\n"); - - fprintf(out, " event.end();\n\n"); - } else if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, - " event.AppendCharArray(arg%d.arg, " - "arg%d.arg_length);\n", - argIndex, argIndex); - } else { - if (*arg == JAVA_TYPE_STRING) { - fprintf(out, " if (arg%d == NULL) {\n", argIndex); - fprintf(out, " arg%d = \"\";\n", argIndex); - fprintf(out, " }\n"); - } - fprintf(out, " event << arg%d;\n", argIndex); - } - argIndex++; - } - - fprintf(out, " return event.write(LOG_ID_STATS);\n"); - fprintf(out, " } else {\n"); - fprintf(out, " return 1;\n"); - fprintf(out, " }\n"); - fprintf(out, "}\n"); - fprintf(out, "\n"); - } - -} - -void write_native_stats_write_methods_q(FILE* out, const string& methodName, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName, const string& tryMethodName) { - for (auto signature_to_modules_it = atoms.signatures_to_modules.begin(); - signature_to_modules_it != atoms.signatures_to_modules.end(); - signature_to_modules_it++) { - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { - continue; - } - vector<java_type_t> signature = signature_to_modules_it->first; - - write_native_method_signature(out, methodName, signature, attributionDecl, " {"); - - write_native_stats_write_body_q(out, signature, attributionDecl, " ", tryMethodName); - fprintf(out, "}\n\n"); - } -} - -void write_native_stats_write_non_chained_methods_q(FILE* out, const string& methodName, - const Atoms& atoms, const AtomDecl& attributionDecl, const string& moduleName, - const string& tryMethodName) { - for (auto signature_it = atoms.non_chained_signatures_to_modules.begin(); - signature_it != atoms.non_chained_signatures_to_modules.end(); signature_it++) { - if (!signature_needed_for_module(signature_it->second, moduleName)) { - continue; - } - vector<java_type_t> signature = signature_it->first; - - write_native_method_signature(out, methodName, signature, attributionDecl, " {"); - - write_native_stats_write_body_q(out, signature, attributionDecl, " ", tryMethodName); - fprintf(out, "}\n\n"); - } -} - -void write_native_try_stats_write_non_chained_methods_q(FILE* out, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName) { - for (auto signature_it = atoms.non_chained_signatures_to_modules.begin(); - signature_it != atoms.non_chained_signatures_to_modules.end(); signature_it++) { - if (!signature_needed_for_module(signature_it->second, moduleName)) { - continue; - } - vector<java_type_t> signature = signature_it->first; - - write_native_method_signature(out, "static int try_stats_write_non_chained", signature, - attributionDecl, " {"); - - int argIndex = 1; - fprintf(out, " if (kStatsdEnabled) {\n"); - fprintf(out, " stats_event_list event(kStatsEventTag);\n"); - fprintf(out, " event << get_elapsed_realtime_ns();\n\n"); - fprintf(out, " event << code;\n\n"); - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (argIndex == 1) { - fprintf(out, " event.begin();\n\n"); - fprintf(out, " event.begin();\n"); - } - if (*arg == JAVA_TYPE_STRING) { - fprintf(out, " if (arg%d == NULL) {\n", argIndex); - fprintf(out, " arg%d = \"\";\n", argIndex); - fprintf(out, " }\n"); - } - if (*arg == JAVA_TYPE_BYTE_ARRAY) { - fprintf(out, - " event.AppendCharArray(arg%d.arg, " - "arg%d.arg_length);\n", - argIndex, argIndex); - } else { - fprintf(out, " event << arg%d;\n", argIndex); - } - if (argIndex == 2) { - fprintf(out, " event.end();\n\n"); - fprintf(out, " event.end();\n\n"); - } - argIndex++; - } - - fprintf(out, " return event.write(LOG_ID_STATS);\n"); - fprintf(out, " } else {\n"); - fprintf(out, " return 1;\n"); - fprintf(out, " }\n"); - fprintf(out, "}\n"); - fprintf(out, "\n"); - } -} - -} // namespace stats_log_api_gen -} // namespace android diff --git a/tools/stats_log_api_gen/native_writer_q.h b/tools/stats_log_api_gen/native_writer_q.h deleted file mode 100644 index a2ab1ae5d5e2..000000000000 --- a/tools/stats_log_api_gen/native_writer_q.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2019, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "Collation.h" - -#include <stdio.h> -#include <string.h> - -namespace android { -namespace stats_log_api_gen { - -using namespace std; - -void write_native_cpp_includes_q(FILE* out); - -void write_native_stats_log_cpp_globals_q(FILE* out); - -void write_native_try_stats_write_methods_q(FILE* out, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName); - -void write_native_stats_write_methods_q(FILE* out, const string& methodName, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName, const string& tryMethodName); - -void write_native_try_stats_write_non_chained_methods_q(FILE* out, const Atoms& atoms, - const AtomDecl& attributionDecl, const string& moduleName); - -void write_native_stats_write_non_chained_methods_q(FILE* out, const string& methodName, - const Atoms& atoms, const AtomDecl& attributionDecl, const string& moduleName, - const string& tryMethodName); - -void write_native_get_timestamp_ns_q(FILE* out); - -} // namespace stats_log_api_gen -} // namespace android diff --git a/tools/stats_log_api_gen/test.proto b/tools/stats_log_api_gen/test.proto index c3e703826be5..aaa488e44fee 100644 --- a/tools/stats_log_api_gen/test.proto +++ b/tools/stats_log_api_gen/test.proto @@ -41,21 +41,20 @@ enum AnEnum { message AllTypesAtom { repeated android.os.statsd.AttributionNode attribution_chain = 1; - optional double double_field = 2; - optional float float_field = 3; - optional int64 int64_field = 4; - optional uint64 uint64_field = 5; - optional int32 int32_field = 6; - optional fixed64 fixed64_field = 7; - optional fixed32 fixed32_field = 8; - optional bool bool_field = 9; - optional string string_field = 10; - optional uint32 uint32_field = 11; - optional AnEnum enum_field = 12; - optional sfixed32 sfixed32_field = 13; - optional sfixed64 sfixed64_field = 14; - optional sint32 sint32_field = 15; - optional sint64 sint64_field = 16; + optional float float_field = 2; + optional int64 int64_field = 3; + optional uint64 uint64_field = 4; + optional int32 int32_field = 5; + optional fixed64 fixed64_field = 6; + optional fixed32 fixed32_field = 7; + optional bool bool_field = 8; + optional string string_field = 9; + optional uint32 uint32_field = 10; + optional AnEnum enum_field = 11; + optional sfixed32 sfixed32_field = 12; + optional sfixed64 sfixed64_field = 13; + optional sint32 sint32_field = 14; + optional sint64 sint64_field = 15; } message Event { @@ -70,6 +69,8 @@ message Event { message BadTypesAtom { optional IntAtom bad_int_atom = 1; optional bytes bad_bytes = 2; + repeated int32 repeated_field = 3; + optional double double_field = 4; } message BadTypesEvent { @@ -148,89 +149,67 @@ message GoodStateAtoms { // The atom has only primary field but no exclusive state field. message BadStateAtom1 { - optional int32 uid = 1 - [(android.os.statsd.state_field_option).option = PRIMARY]; + optional int32 uid = 1 [(android.os.statsd.state_field_option).primary_field = true]; } // Only primative types can be annotated. message BadStateAtom2 { repeated android.os.statsd.AttributionNode attribution = 1 - [(android.os.statsd.state_field_option).option = PRIMARY]; - optional int32 state = 2 - [(android.os.statsd.state_field_option).option = EXCLUSIVE]; + [(android.os.statsd.state_field_option).primary_field = true]; + optional int32 state = 2 [(android.os.statsd.state_field_option).exclusive_state = true]; } // Having 2 exclusive state field in the atom means the atom is badly designed. // E.g., putting bluetooth state and wifi state in the same atom. message BadStateAtom3 { - optional int32 uid = 1 - [(android.os.statsd.state_field_option).option = PRIMARY]; - optional int32 state = 2 - [(android.os.statsd.state_field_option).option = EXCLUSIVE]; - optional int32 state2 = 3 - [(android.os.statsd.state_field_option).option = EXCLUSIVE]; + optional int32 uid = 1 [(android.os.statsd.state_field_option).primary_field = true]; + optional int32 state = 2 [(android.os.statsd.state_field_option).exclusive_state = true]; + optional int32 state2 = 3 [(android.os.statsd.state_field_option).exclusive_state = true]; } message GoodStateAtom1 { - optional int32 uid = 1 - [(android.os.statsd.state_field_option).option = PRIMARY]; - optional int32 state = 2 - [(android.os.statsd.state_field_option).option = EXCLUSIVE]; + optional int32 uid = 1 [(android.os.statsd.state_field_option).primary_field = true]; + optional int32 state = 2 [(android.os.statsd.state_field_option).exclusive_state = true]; } // Atoms can have exclusive state field, but no primary field. That means // the state is globally exclusive (e.g., DisplayState). message GoodStateAtom2 { optional int32 uid = 1; - optional int32 state = 2 - [(android.os.statsd.state_field_option).option = EXCLUSIVE]; + optional int32 state = 2 [(android.os.statsd.state_field_option).exclusive_state = true]; } // We can have more than one primary fields. That means their combination is a // primary key. message GoodStateAtom3 { - optional int32 uid = 1 - [(android.os.statsd.state_field_option).option = PRIMARY]; - optional int32 tid = 2 - [(android.os.statsd.state_field_option).option = PRIMARY]; - optional int32 state = 3 - [(android.os.statsd.state_field_option).option = EXCLUSIVE]; -} - -message WhitelistedAtom { - optional int32 field = 1; -} - -message NonWhitelistedAtom { - optional int32 field = 1; -} - -message ListedAtoms { - oneof event { - // Atoms can be whitelisted i.e. they can be triggered by any source - WhitelistedAtom whitelisted_atom = 1 [(android.os.statsd.allow_from_any_uid) = true]; - // Atoms are not whitelisted by default, so they can only be triggered - // by whitelisted sources - NonWhitelistedAtom non_whitelisted_atom = 2; - } + optional int32 uid = 1 [(android.os.statsd.state_field_option).primary_field = true]; + optional int32 tid = 2 [(android.os.statsd.state_field_option).primary_field = true]; + optional int32 state = 3 [(android.os.statsd.state_field_option).exclusive_state = true]; } message ModuleOneAtom { - optional int32 field = 1; + optional int32 field = 1 [(android.os.statsd.is_uid) = true]; } message ModuleTwoAtom { optional int32 field = 1; } +message ModuleOneAndTwoAtom { + optional int32 field = 1 [(android.os.statsd.state_field_option).exclusive_state = true]; +} + message NoModuleAtom { optional string field = 1; } message ModuleAtoms { oneof event { - ModuleOneAtom module_one_atom = 1 [(android.os.statsd.log_from_module) = "module1"]; - ModuleTwoAtom module_two_atom = 2 [(android.os.statsd.log_from_module) = "module2"]; - NoModuleAtom no_module_atom = 3; + ModuleOneAtom module_one_atom = 1 [(android.os.statsd.module) = "module1"]; + ModuleTwoAtom module_two_atom = 2 [(android.os.statsd.module) = "module2"]; + ModuleOneAndTwoAtom module_one_and_two_atom = 3 [ + (android.os.statsd.module) = "module1", (android.os.statsd.module) = "module2" + ]; + NoModuleAtom no_module_atom = 4; } -}
\ No newline at end of file +} diff --git a/tools/stats_log_api_gen/test_collation.cpp b/tools/stats_log_api_gen/test_collation.cpp index bcf18ae8bf19..dbae58889333 100644 --- a/tools/stats_log_api_gen/test_collation.cpp +++ b/tools/stats_log_api_gen/test_collation.cpp @@ -15,11 +15,10 @@ */ #include <gtest/gtest.h> +#include <stdio.h> -#include "frameworks/base/tools/stats_log_api_gen/test.pb.h" #include "Collation.h" - -#include <stdio.h> +#include "frameworks/base/tools/stats_log_api_gen/test.pb.h" namespace android { namespace stats_log_api_gen { @@ -29,16 +28,14 @@ using std::set; using std::vector; /** - * Return whether the set contains a vector of the elements provided. + * Return whether the map contains a vector of the elements provided. */ -static bool -set_contains_vector(const map<vector<java_type_t>, set<string>>& s, int count, ...) -{ +static bool map_contains_vector(const SignatureInfoMap& s, int count, ...) { va_list args; vector<java_type_t> v; va_start(args, count); - for (int i=0; i<count; i++) { + for (int i = 0; i < count; i++) { v.push_back((java_type_t)va_arg(args, int)); } va_end(args); @@ -47,103 +44,102 @@ set_contains_vector(const map<vector<java_type_t>, set<string>>& s, int count, . } /** - * Expect that the provided set contains the elements provided. + * Expect that the provided map contains the elements provided. */ -#define EXPECT_SET_CONTAINS_SIGNATURE(s, ...) \ - do { \ - int count = sizeof((int[]){__VA_ARGS__})/sizeof(int); \ - EXPECT_TRUE(set_contains_vector(s, count, __VA_ARGS__)); \ - } while(0) +#define EXPECT_MAP_CONTAINS_SIGNATURE(s, ...) \ + do { \ + int count = sizeof((int[]){__VA_ARGS__}) / sizeof(int); \ + EXPECT_TRUE(map_contains_vector(s, count, __VA_ARGS__)); \ + } while (0) /** Expects that the provided atom has no enum values for any field. */ -#define EXPECT_NO_ENUM_FIELD(atom) \ - do { \ +#define EXPECT_NO_ENUM_FIELD(atom) \ + do { \ for (vector<AtomField>::const_iterator field = atom->fields.begin(); \ - field != atom->fields.end(); field++) { \ - EXPECT_TRUE(field->enumValues.empty()); \ - } \ - } while(0) + field != atom->fields.end(); field++) { \ + EXPECT_TRUE(field->enumValues.empty()); \ + } \ + } while (0) /** Expects that exactly one specific field has expected enum values. */ -#define EXPECT_HAS_ENUM_FIELD(atom, field_name, values) \ - do { \ +#define EXPECT_HAS_ENUM_FIELD(atom, field_name, values) \ + do { \ for (vector<AtomField>::const_iterator field = atom->fields.begin(); \ - field != atom->fields.end(); field++) { \ - if (field->name == field_name) { \ - EXPECT_EQ(field->enumValues, values); \ - } else { \ - EXPECT_TRUE(field->enumValues.empty()); \ - } \ - } \ - } while(0) - + field != atom->fields.end(); field++) { \ + if (field->name == field_name) { \ + EXPECT_EQ(field->enumValues, values); \ + } else { \ + EXPECT_TRUE(field->enumValues.empty()); \ + } \ + } \ + } while (0) /** * Test a correct collation, with all the types. */ TEST(CollationTest, CollateStats) { Atoms atoms; - int errorCount = collate_atoms(Event::descriptor(), &atoms); + int errorCount = collate_atoms(Event::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(0, errorCount); - EXPECT_EQ(3ul, atoms.signatures_to_modules.size()); + EXPECT_EQ(3ul, atoms.signatureInfoMap.size()); // IntAtom, AnotherIntAtom - EXPECT_SET_CONTAINS_SIGNATURE(atoms.signatures_to_modules, JAVA_TYPE_INT); + EXPECT_MAP_CONTAINS_SIGNATURE(atoms.signatureInfoMap, JAVA_TYPE_INT); // OutOfOrderAtom - EXPECT_SET_CONTAINS_SIGNATURE(atoms.signatures_to_modules, JAVA_TYPE_INT, JAVA_TYPE_INT); + EXPECT_MAP_CONTAINS_SIGNATURE(atoms.signatureInfoMap, JAVA_TYPE_INT, JAVA_TYPE_INT); // AllTypesAtom - EXPECT_SET_CONTAINS_SIGNATURE( - atoms.signatures_to_modules, - JAVA_TYPE_ATTRIBUTION_CHAIN, // AttributionChain - JAVA_TYPE_DOUBLE, // double - JAVA_TYPE_FLOAT, // float - JAVA_TYPE_LONG, // int64 - JAVA_TYPE_LONG, // uint64 - JAVA_TYPE_INT, // int32 - JAVA_TYPE_LONG, // fixed64 - JAVA_TYPE_INT, // fixed32 - JAVA_TYPE_BOOLEAN, // bool - JAVA_TYPE_STRING, // string - JAVA_TYPE_INT, // uint32 - JAVA_TYPE_INT, // AnEnum - JAVA_TYPE_INT, // sfixed32 - JAVA_TYPE_LONG, // sfixed64 - JAVA_TYPE_INT, // sint32 - JAVA_TYPE_LONG // sint64 + EXPECT_MAP_CONTAINS_SIGNATURE(atoms.signatureInfoMap, + JAVA_TYPE_ATTRIBUTION_CHAIN, // AttributionChain + JAVA_TYPE_FLOAT, // float + JAVA_TYPE_LONG, // int64 + JAVA_TYPE_LONG, // uint64 + JAVA_TYPE_INT, // int32 + JAVA_TYPE_LONG, // fixed64 + JAVA_TYPE_INT, // fixed32 + JAVA_TYPE_BOOLEAN, // bool + JAVA_TYPE_STRING, // string + JAVA_TYPE_INT, // uint32 + JAVA_TYPE_INT, // AnEnum + JAVA_TYPE_INT, // sfixed32 + JAVA_TYPE_LONG, // sfixed64 + JAVA_TYPE_INT, // sint32 + JAVA_TYPE_LONG // sint64 ); - set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - EXPECT_EQ(1, atom->code); - EXPECT_EQ("int_atom", atom->name); - EXPECT_EQ("IntAtom", atom->message); - EXPECT_NO_ENUM_FIELD(atom); - atom++; - - EXPECT_EQ(2, atom->code); - EXPECT_EQ("out_of_order_atom", atom->name); - EXPECT_EQ("OutOfOrderAtom", atom->message); - EXPECT_NO_ENUM_FIELD(atom); - atom++; - - EXPECT_EQ(3, atom->code); - EXPECT_EQ("another_int_atom", atom->name); - EXPECT_EQ("AnotherIntAtom", atom->message); - EXPECT_NO_ENUM_FIELD(atom); - atom++; - - EXPECT_EQ(4, atom->code); - EXPECT_EQ("all_types_atom", atom->name); - EXPECT_EQ("AllTypesAtom", atom->message); + EXPECT_EQ(4ul, atoms.decls.size()); + + AtomDeclSet::const_iterator atomIt = atoms.decls.begin(); + EXPECT_EQ(1, (*atomIt)->code); + EXPECT_EQ("int_atom", (*atomIt)->name); + EXPECT_EQ("IntAtom", (*atomIt)->message); + EXPECT_NO_ENUM_FIELD((*atomIt)); + atomIt++; + + EXPECT_EQ(2, (*atomIt)->code); + EXPECT_EQ("out_of_order_atom", (*atomIt)->name); + EXPECT_EQ("OutOfOrderAtom", (*atomIt)->message); + EXPECT_NO_ENUM_FIELD((*atomIt)); + atomIt++; + + EXPECT_EQ(3, (*atomIt)->code); + EXPECT_EQ("another_int_atom", (*atomIt)->name); + EXPECT_EQ("AnotherIntAtom", (*atomIt)->message); + EXPECT_NO_ENUM_FIELD((*atomIt)); + atomIt++; + + EXPECT_EQ(4, (*atomIt)->code); + EXPECT_EQ("all_types_atom", (*atomIt)->name); + EXPECT_EQ("AllTypesAtom", (*atomIt)->message); map<int, string> enumValues; enumValues[0] = "VALUE0"; enumValues[1] = "VALUE1"; - EXPECT_HAS_ENUM_FIELD(atom, "enum_field", enumValues); - atom++; + EXPECT_HAS_ENUM_FIELD((*atomIt), "enum_field", enumValues); + atomIt++; - EXPECT_TRUE(atom == atoms.decls.end()); + EXPECT_EQ(atoms.decls.end(), atomIt); } /** @@ -151,19 +147,20 @@ TEST(CollationTest, CollateStats) { */ TEST(CollationTest, NonMessageTypeFails) { Atoms atoms; - int errorCount = collate_atoms(IntAtom::descriptor(), &atoms); + int errorCount = collate_atoms(IntAtom::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(1, errorCount); } /** - * Test that atoms that have non-primitive types are rejected. + * Test that atoms that have non-primitive types or repeated fields are + * rejected. */ TEST(CollationTest, FailOnBadTypes) { Atoms atoms; - int errorCount = collate_atoms(BadTypesEvent::descriptor(), &atoms); + int errorCount = collate_atoms(BadTypesEvent::descriptor(), DEFAULT_MODULE_NAME, &atoms); - EXPECT_EQ(2, errorCount); + EXPECT_EQ(4, errorCount); } /** @@ -171,18 +168,20 @@ TEST(CollationTest, FailOnBadTypes) { */ TEST(CollationTest, FailOnSkippedFieldsSingle) { Atoms atoms; - int errorCount = collate_atoms(BadSkippedFieldSingle::descriptor(), &atoms); + int errorCount = + collate_atoms(BadSkippedFieldSingle::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(1, errorCount); } /** - * Test that atoms that skip field numbers (not in the first position, and multiple - * times) are rejected. + * Test that atoms that skip field numbers (not in the first position, and + * multiple times) are rejected. */ TEST(CollationTest, FailOnSkippedFieldsMultiple) { Atoms atoms; - int errorCount = collate_atoms(BadSkippedFieldMultiple::descriptor(), &atoms); + int errorCount = + collate_atoms(BadSkippedFieldMultiple::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(2, errorCount); } @@ -192,99 +191,179 @@ TEST(CollationTest, FailOnSkippedFieldsMultiple) { * rejected. */ TEST(CollationTest, FailBadAttributionNodePosition) { - Atoms atoms; - int errorCount = - collate_atoms(BadAttributionNodePosition::descriptor(), &atoms); + Atoms atoms; + int errorCount = + collate_atoms(BadAttributionNodePosition::descriptor(), DEFAULT_MODULE_NAME, &atoms); - EXPECT_EQ(1, errorCount); + EXPECT_EQ(1, errorCount); } TEST(CollationTest, FailOnBadStateAtomOptions) { Atoms atoms; - int errorCount = collate_atoms(BadStateAtoms::descriptor(), &atoms); + int errorCount = collate_atoms(BadStateAtoms::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(3, errorCount); } TEST(CollationTest, PassOnGoodStateAtomOptions) { Atoms atoms; - int errorCount = collate_atoms(GoodStateAtoms::descriptor(), &atoms); + int errorCount = collate_atoms(GoodStateAtoms::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(0, errorCount); } TEST(CollationTest, PassOnGoodBinaryFieldAtom) { Atoms atoms; int errorCount = - collate_atoms(GoodEventWithBinaryFieldAtom::descriptor(), &atoms); + collate_atoms(GoodEventWithBinaryFieldAtom::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(0, errorCount); } TEST(CollationTest, FailOnBadBinaryFieldAtom) { Atoms atoms; int errorCount = - collate_atoms(BadEventWithBinaryFieldAtom::descriptor(), &atoms); + collate_atoms(BadEventWithBinaryFieldAtom::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_TRUE(errorCount > 0); } -TEST(CollationTest, PassOnWhitelistedAtom) { +TEST(CollationTest, PassOnLogFromModuleAtom) { Atoms atoms; - int errorCount = collate_atoms(ListedAtoms::descriptor(), &atoms); + int errorCount = collate_atoms(ModuleAtoms::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(errorCount, 0); - EXPECT_EQ(atoms.decls.size(), 2ul); + EXPECT_EQ(atoms.decls.size(), 4ul); } -TEST(CollationTest, RecogniseWhitelistedAtom) { - Atoms atoms; - collate_atoms(ListedAtoms::descriptor(), &atoms); - for (const auto& atomDecl : atoms.decls) { - if (atomDecl.code == 1) { - EXPECT_TRUE(atomDecl.whitelisted); - } else { - EXPECT_FALSE(atomDecl.whitelisted); - } - } -} - -TEST(CollationTest, PassOnLogFromModuleAtom) { +TEST(CollationTest, RecognizeModuleAtom) { Atoms atoms; - int errorCount = collate_atoms(ModuleAtoms::descriptor(), &atoms); + int errorCount = collate_atoms(ModuleAtoms::descriptor(), DEFAULT_MODULE_NAME, &atoms); EXPECT_EQ(errorCount, 0); - EXPECT_EQ(atoms.decls.size(), 3ul); + EXPECT_EQ(atoms.decls.size(), 4ul); + EXPECT_EQ(atoms.signatureInfoMap.size(), 2u); + EXPECT_MAP_CONTAINS_SIGNATURE(atoms.signatureInfoMap, JAVA_TYPE_INT); + EXPECT_MAP_CONTAINS_SIGNATURE(atoms.signatureInfoMap, JAVA_TYPE_STRING); + + SignatureInfoMap::const_iterator signatureInfoMapIt; + const vector<java_type_t>* signature; + const FieldNumberToAtomDeclSet* fieldNumberToAtomDeclSet; + FieldNumberToAtomDeclSet::const_iterator fieldNumberToAtomDeclSetIt; + const AtomDeclSet* atomDeclSet; + AtomDeclSet::const_iterator atomDeclSetIt; + AtomDecl* atomDecl; + FieldNumberToAnnotations* fieldNumberToAnnotations; + FieldNumberToAnnotations::const_iterator fieldNumberToAnnotationsIt; + const AnnotationSet* annotationSet; + AnnotationSet::const_iterator annotationSetIt; + Annotation* annotation; + + signatureInfoMapIt = atoms.signatureInfoMap.begin(); + signature = &(signatureInfoMapIt->first); + fieldNumberToAtomDeclSet = &signatureInfoMapIt->second; + EXPECT_EQ(1ul, signature->size()); + EXPECT_EQ(JAVA_TYPE_INT, signature->at(0)); + EXPECT_EQ(1ul, fieldNumberToAtomDeclSet->size()); + fieldNumberToAtomDeclSetIt = fieldNumberToAtomDeclSet->begin(); + EXPECT_EQ(1, fieldNumberToAtomDeclSetIt->first); + atomDeclSet = &fieldNumberToAtomDeclSetIt->second; + EXPECT_EQ(2ul, atomDeclSet->size()); + atomDeclSetIt = atomDeclSet->begin(); + atomDecl = atomDeclSetIt->get(); + EXPECT_EQ(1, atomDecl->code); + fieldNumberToAnnotations = &atomDecl->fieldNumberToAnnotations; + fieldNumberToAnnotationsIt = fieldNumberToAnnotations->find(1); + EXPECT_NE(fieldNumberToAnnotations->end(), fieldNumberToAnnotationsIt); + annotationSet = &fieldNumberToAnnotationsIt->second; + EXPECT_EQ(1ul, annotationSet->size()); + annotationSetIt = annotationSet->begin(); + annotation = annotationSetIt->get(); + EXPECT_EQ(ANNOTATION_ID_IS_UID, annotation->annotationId); + EXPECT_EQ(1, annotation->atomId); + EXPECT_EQ(ANNOTATION_TYPE_BOOL, annotation->type); + EXPECT_TRUE(annotation->value.boolValue); + + atomDeclSetIt++; + atomDecl = atomDeclSetIt->get(); + EXPECT_EQ(3, atomDecl->code); + fieldNumberToAnnotations = &atomDecl->fieldNumberToAnnotations; + fieldNumberToAnnotationsIt = fieldNumberToAnnotations->find(1); + EXPECT_NE(fieldNumberToAnnotations->end(), fieldNumberToAnnotationsIt); + annotationSet = &fieldNumberToAnnotationsIt->second; + EXPECT_EQ(1ul, annotationSet->size()); + annotationSetIt = annotationSet->begin(); + annotation = annotationSetIt->get(); + EXPECT_EQ(ANNOTATION_ID_EXCLUSIVE_STATE, annotation->annotationId); + EXPECT_EQ(3, annotation->atomId); + EXPECT_EQ(ANNOTATION_TYPE_BOOL, annotation->type); + EXPECT_TRUE(annotation->value.boolValue); + + signatureInfoMapIt++; + signature = &signatureInfoMapIt->first; + fieldNumberToAtomDeclSet = &signatureInfoMapIt->second; + EXPECT_EQ(1ul, signature->size()); + EXPECT_EQ(JAVA_TYPE_STRING, signature->at(0)); + EXPECT_EQ(0ul, fieldNumberToAtomDeclSet->size()); } -TEST(CollationTest, RecognizeModuleAtom) { +TEST(CollationTest, RecognizeModule1Atom) { Atoms atoms; - int errorCount = collate_atoms(ModuleAtoms::descriptor(), &atoms); + const string moduleName = "module1"; + int errorCount = collate_atoms(ModuleAtoms::descriptor(), moduleName, &atoms); EXPECT_EQ(errorCount, 0); - EXPECT_EQ(atoms.decls.size(), 3ul); - for (const auto& atomDecl: atoms.decls) { - if (atomDecl.code == 1) { - EXPECT_TRUE(atomDecl.hasModule); - EXPECT_EQ(atomDecl.moduleName, "module1"); - } else if (atomDecl.code == 2) { - EXPECT_TRUE(atomDecl.hasModule); - EXPECT_EQ(atomDecl.moduleName, "module2"); - } else { - EXPECT_FALSE(atomDecl.hasModule); - } - } - - EXPECT_EQ(atoms.signatures_to_modules.size(), 2u); - EXPECT_SET_CONTAINS_SIGNATURE(atoms.signatures_to_modules, JAVA_TYPE_INT); - EXPECT_SET_CONTAINS_SIGNATURE(atoms.signatures_to_modules, JAVA_TYPE_STRING); - for (auto signature_to_modules_it : atoms.signatures_to_modules) { - vector<java_type_t> signature = signature_to_modules_it.first; - if (signature[0] == JAVA_TYPE_STRING) { - EXPECT_EQ(signature_to_modules_it.second.size(), 0u); - } else if (signature[0] == JAVA_TYPE_INT) { - set<string> modules = signature_to_modules_it.second; - EXPECT_EQ(modules.size(), 2u); - // Assert that the set contains "module1" and "module2". - EXPECT_NE(modules.find("module1"), modules.end()); - EXPECT_NE(modules.find("module2"), modules.end()); - } - } + EXPECT_EQ(atoms.decls.size(), 2ul); + EXPECT_EQ(atoms.signatureInfoMap.size(), 1u); + EXPECT_MAP_CONTAINS_SIGNATURE(atoms.signatureInfoMap, JAVA_TYPE_INT); + + SignatureInfoMap::const_iterator signatureInfoMapIt; + const vector<java_type_t>* signature; + const FieldNumberToAtomDeclSet* fieldNumberToAtomDeclSet; + FieldNumberToAtomDeclSet::const_iterator fieldNumberToAtomDeclSetIt; + const AtomDeclSet* atomDeclSet; + AtomDeclSet::const_iterator atomDeclSetIt; + AtomDecl* atomDecl; + FieldNumberToAnnotations* fieldNumberToAnnotations; + FieldNumberToAnnotations::const_iterator fieldNumberToAnnotationsIt; + const AnnotationSet* annotationSet; + AnnotationSet::const_iterator annotationSetIt; + Annotation* annotation; + + signatureInfoMapIt = atoms.signatureInfoMap.begin(); + signature = &(signatureInfoMapIt->first); + fieldNumberToAtomDeclSet = &signatureInfoMapIt->second; + EXPECT_EQ(1ul, signature->size()); + EXPECT_EQ(JAVA_TYPE_INT, signature->at(0)); + EXPECT_EQ(1ul, fieldNumberToAtomDeclSet->size()); + fieldNumberToAtomDeclSetIt = fieldNumberToAtomDeclSet->begin(); + EXPECT_EQ(1, fieldNumberToAtomDeclSetIt->first); + atomDeclSet = &fieldNumberToAtomDeclSetIt->second; + EXPECT_EQ(2ul, atomDeclSet->size()); + atomDeclSetIt = atomDeclSet->begin(); + atomDecl = atomDeclSetIt->get(); + EXPECT_EQ(1, atomDecl->code); + fieldNumberToAnnotations = &atomDecl->fieldNumberToAnnotations; + fieldNumberToAnnotationsIt = fieldNumberToAnnotations->find(1); + EXPECT_NE(fieldNumberToAnnotations->end(), fieldNumberToAnnotationsIt); + annotationSet = &fieldNumberToAnnotationsIt->second; + EXPECT_EQ(1ul, annotationSet->size()); + annotationSetIt = annotationSet->begin(); + annotation = annotationSetIt->get(); + EXPECT_EQ(ANNOTATION_ID_IS_UID, annotation->annotationId); + EXPECT_EQ(1, annotation->atomId); + EXPECT_EQ(ANNOTATION_TYPE_BOOL, annotation->type); + EXPECT_TRUE(annotation->value.boolValue); + + atomDeclSetIt++; + atomDecl = atomDeclSetIt->get(); + EXPECT_EQ(3, atomDecl->code); + fieldNumberToAnnotations = &atomDecl->fieldNumberToAnnotations; + fieldNumberToAnnotationsIt = fieldNumberToAnnotations->find(1); + EXPECT_NE(fieldNumberToAnnotations->end(), fieldNumberToAnnotationsIt); + annotationSet = &fieldNumberToAnnotationsIt->second; + EXPECT_EQ(1ul, annotationSet->size()); + annotationSetIt = annotationSet->begin(); + annotation = annotationSetIt->get(); + EXPECT_EQ(ANNOTATION_ID_EXCLUSIVE_STATE, annotation->annotationId); + EXPECT_EQ(3, annotation->atomId); + EXPECT_EQ(ANNOTATION_TYPE_BOOL, annotation->type); + EXPECT_TRUE(annotation->value.boolValue); } } // namespace stats_log_api_gen -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/tools/stats_log_api_gen/utils.cpp b/tools/stats_log_api_gen/utils.cpp index 641404280093..abb89133e58e 100644 --- a/tools/stats_log_api_gen/utils.cpp +++ b/tools/stats_log_api_gen/utils.cpp @@ -22,10 +22,10 @@ namespace android { namespace stats_log_api_gen { static void build_non_chained_decl_map(const Atoms& atoms, - std::map<int, set<AtomDecl>::const_iterator>* decl_map) { - for (set<AtomDecl>::const_iterator atom = atoms.non_chained_decls.begin(); - atom != atoms.non_chained_decls.end(); atom++) { - decl_map->insert(std::make_pair(atom->code, atom)); + std::map<int, AtomDeclSet::const_iterator>* decl_map) { + for (AtomDeclSet::const_iterator atomIt = atoms.non_chained_decls.begin(); + atomIt != atoms.non_chained_decls.end(); atomIt++) { + decl_map->insert(std::make_pair((*atomIt)->code, atomIt)); } } @@ -36,7 +36,7 @@ string make_constant_name(const string& str) { string result; const int N = str.size(); bool underscore_next = false; - for (int i=0; i<N; i++) { + for (int i = 0; i < N; i++) { char c = str[i]; if (c >= 'A' && c <= 'Z') { if (underscore_next) { @@ -98,22 +98,9 @@ const char* java_type_name(java_type_t type) { } } -bool atom_needed_for_module(const AtomDecl& atomDecl, const string& moduleName) { - if (moduleName == DEFAULT_MODULE_NAME) { - return true; - } - return atomDecl.hasModule && (moduleName == atomDecl.moduleName); -} - -bool signature_needed_for_module(const set<string>& modules, const string& moduleName) { - if (moduleName == DEFAULT_MODULE_NAME) { - return true; - } - return modules.find(moduleName) != modules.end(); -} - // Native -// Writes namespaces for the cpp and header files, returning the number of namespaces written. +// Writes namespaces for the cpp and header files, returning the number of +// namespaces written. void write_namespace(FILE* out, const string& cppNamespaces) { vector<string> cppNamespaceVec = android::base::Split(cppNamespaces, ","); for (string cppNamespace : cppNamespaceVec) { @@ -129,35 +116,31 @@ void write_closing_namespace(FILE* out, const string& cppNamespaces) { } } -static void write_cpp_usage( - FILE* out, const string& method_name, const string& atom_code_name, - const AtomDecl& atom, const AtomDecl &attributionDecl) { - fprintf(out, " * Usage: %s(StatsLog.%s", method_name.c_str(), - atom_code_name.c_str()); +static void write_cpp_usage(FILE* out, const string& method_name, const string& atom_code_name, + const shared_ptr<AtomDecl> atom, const AtomDecl& attributionDecl) { + fprintf(out, " * Usage: %s(StatsLog.%s", method_name.c_str(), atom_code_name.c_str()); - for (vector<AtomField>::const_iterator field = atom.fields.begin(); - field != atom.fields.end(); field++) { + for (vector<AtomField>::const_iterator field = atom->fields.begin(); + field != atom->fields.end(); field++) { if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (auto chainField : attributionDecl.fields) { if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, ", const std::vector<%s>& %s", - cpp_type_name(chainField.javaType), - chainField.name.c_str()); + fprintf(out, ", const std::vector<%s>& %s", cpp_type_name(chainField.javaType), + chainField.name.c_str()); } else { fprintf(out, ", const %s* %s, size_t %s_length", - cpp_type_name(chainField.javaType), - chainField.name.c_str(), chainField.name.c_str()); + cpp_type_name(chainField.javaType), chainField.name.c_str(), + chainField.name.c_str()); } } } else if (field->javaType == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, ", const std::map<int, int32_t>& %s_int" - ", const std::map<int, int64_t>& %s_long" - ", const std::map<int, char const*>& %s_str" - ", const std::map<int, float>& %s_float", - field->name.c_str(), - field->name.c_str(), - field->name.c_str(), - field->name.c_str()); + fprintf(out, + ", const std::map<int, int32_t>& %s_int" + ", const std::map<int, int64_t>& %s_long" + ", const std::map<int, char const*>& %s_str" + ", const std::map<int, float>& %s_float", + field->name.c_str(), field->name.c_str(), field->name.c_str(), + field->name.c_str()); } else { fprintf(out, ", %s %s", cpp_type_name(field->javaType), field->name.c_str()); } @@ -165,38 +148,33 @@ static void write_cpp_usage( fprintf(out, ");\n"); } -void write_native_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, - const string& moduleName) { +void write_native_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl) { fprintf(out, "/**\n"); fprintf(out, " * Constants for atom codes.\n"); fprintf(out, " */\n"); fprintf(out, "enum {\n"); - std::map<int, set<AtomDecl>::const_iterator> atom_code_to_non_chained_decl_map; + std::map<int, AtomDeclSet::const_iterator> atom_code_to_non_chained_decl_map; build_non_chained_decl_map(atoms, &atom_code_to_non_chained_decl_map); size_t i = 0; // Print atom constants - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - // Skip if the atom is not needed for the module. - if (!atom_needed_for_module(*atom, moduleName)) { - continue; - } - string constant = make_constant_name(atom->name); + for (AtomDeclSet::const_iterator atomIt = atoms.decls.begin(); atomIt != atoms.decls.end(); + atomIt++) { + string constant = make_constant_name((*atomIt)->name); fprintf(out, "\n"); fprintf(out, " /**\n"); - fprintf(out, " * %s %s\n", atom->message.c_str(), atom->name.c_str()); - write_cpp_usage(out, "stats_write", constant, *atom, attributionDecl); + fprintf(out, " * %s %s\n", (*atomIt)->message.c_str(), (*atomIt)->name.c_str()); + write_cpp_usage(out, "stats_write", constant, *atomIt, attributionDecl); - auto non_chained_decl = atom_code_to_non_chained_decl_map.find(atom->code); + auto non_chained_decl = atom_code_to_non_chained_decl_map.find((*atomIt)->code); if (non_chained_decl != atom_code_to_non_chained_decl_map.end()) { write_cpp_usage(out, "stats_write_non_chained", constant, *non_chained_decl->second, - attributionDecl); + attributionDecl); } fprintf(out, " */\n"); char const* const comma = (i == atoms.decls.size() - 1) ? "" : ","; - fprintf(out, " %s = %d%s\n", constant.c_str(), atom->code, comma); + fprintf(out, " %s = %d%s\n", constant.c_str(), (*atomIt)->code, comma); i++; } fprintf(out, "\n"); @@ -205,30 +183,30 @@ void write_native_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& } void write_native_method_signature(FILE* out, const string& methodName, - const vector<java_type_t>& signature, const AtomDecl& attributionDecl, - const string& closer) { + const vector<java_type_t>& signature, + const AtomDecl& attributionDecl, const string& closer) { fprintf(out, "%s(int32_t code", methodName.c_str()); int argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (auto chainField : attributionDecl.fields) { if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, ", const std::vector<%s>& %s", - cpp_type_name(chainField.javaType), - chainField.name.c_str()); + fprintf(out, ", const std::vector<%s>& %s", cpp_type_name(chainField.javaType), + chainField.name.c_str()); } else { - fprintf(out, ", const %s* %s, size_t %s_length", - cpp_type_name(chainField.javaType), - chainField.name.c_str(), chainField.name.c_str()); + fprintf(out, ", const %s* %s, size_t %s_length", + cpp_type_name(chainField.javaType), chainField.name.c_str(), + chainField.name.c_str()); } } } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, ", const std::map<int, int32_t>& arg%d_1, " - "const std::map<int, int64_t>& arg%d_2, " - "const std::map<int, char const*>& arg%d_3, " - "const std::map<int, float>& arg%d_4", - argIndex, argIndex, argIndex, argIndex); + fprintf(out, + ", const std::map<int, int32_t>& arg%d_1, " + "const std::map<int, int64_t>& arg%d_2, " + "const std::map<int, char const*>& arg%d_3, " + "const std::map<int, float>& arg%d_4", + argIndex, argIndex, argIndex, argIndex); } else { fprintf(out, ", %s arg%d", cpp_type_name(*arg), argIndex); } @@ -238,86 +216,71 @@ void write_native_method_signature(FILE* out, const string& methodName, } void write_native_method_call(FILE* out, const string& methodName, - const vector<java_type_t>& signature, const AtomDecl& attributionDecl, int argIndex) { + const vector<java_type_t>& signature, const AtomDecl& attributionDecl, + int argIndex) { fprintf(out, "%s(code", methodName.c_str()); - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { - if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - for (auto chainField : attributionDecl.fields) { - if (chainField.javaType == JAVA_TYPE_STRING) { - fprintf(out, ", %s", + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { + if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { + for (auto chainField : attributionDecl.fields) { + if (chainField.javaType == JAVA_TYPE_STRING) { + fprintf(out, ", %s", chainField.name.c_str()); + } else { + fprintf(out, ", %s, %s_length", chainField.name.c_str(), chainField.name.c_str()); - } else { - fprintf(out, ", %s, %s_length", - chainField.name.c_str(), chainField.name.c_str()); - } - } - } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - fprintf(out, ", arg%d_1, arg%d_2, arg%d_3, arg%d_4", argIndex, - argIndex, argIndex, argIndex); - } else { - fprintf(out, ", arg%d", argIndex); - } - argIndex++; + } + } + } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { + fprintf(out, ", arg%d_1, arg%d_2, arg%d_3, arg%d_4", argIndex, argIndex, argIndex, + argIndex); + } else { + fprintf(out, ", arg%d", argIndex); + } + argIndex++; } fprintf(out, ");\n"); } // Java -void write_java_atom_codes(FILE* out, const Atoms& atoms, const string& moduleName) { +void write_java_atom_codes(FILE* out, const Atoms& atoms) { fprintf(out, " // Constants for atom codes.\n"); - std::map<int, set<AtomDecl>::const_iterator> atom_code_to_non_chained_decl_map; + std::map<int, AtomDeclSet::const_iterator> atom_code_to_non_chained_decl_map; build_non_chained_decl_map(atoms, &atom_code_to_non_chained_decl_map); // Print constants for the atom codes. - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - // Skip if the atom is not needed for the module. - if (!atom_needed_for_module(*atom, moduleName)) { - continue; - } - string constant = make_constant_name(atom->name); + for (AtomDeclSet::const_iterator atomIt = atoms.decls.begin(); atomIt != atoms.decls.end(); + atomIt++) { + string constant = make_constant_name((*atomIt)->name); fprintf(out, "\n"); fprintf(out, " /**\n"); - fprintf(out, " * %s %s<br>\n", atom->message.c_str(), atom->name.c_str()); - write_java_usage(out, "write", constant, *atom); - auto non_chained_decl = atom_code_to_non_chained_decl_map.find(atom->code); + fprintf(out, " * %s %s<br>\n", (*atomIt)->message.c_str(), (*atomIt)->name.c_str()); + write_java_usage(out, "write", constant, **atomIt); + auto non_chained_decl = atom_code_to_non_chained_decl_map.find((*atomIt)->code); if (non_chained_decl != atom_code_to_non_chained_decl_map.end()) { - write_java_usage(out, "write_non_chained", constant, *non_chained_decl->second); - } - if (moduleName == DEFAULT_MODULE_NAME) { - fprintf(out, " * @hide\n"); + write_java_usage(out, "write_non_chained", constant, **(non_chained_decl->second)); } fprintf(out, " */\n"); - fprintf(out, " public static final int %s = %d;\n", constant.c_str(), atom->code); + fprintf(out, " public static final int %s = %d;\n", constant.c_str(), (*atomIt)->code); } fprintf(out, "\n"); } -void write_java_enum_values(FILE* out, const Atoms& atoms, const string& moduleName) { +void write_java_enum_values(FILE* out, const Atoms& atoms) { fprintf(out, " // Constants for enum values.\n\n"); - for (set<AtomDecl>::const_iterator atom = atoms.decls.begin(); - atom != atoms.decls.end(); atom++) { - // Skip if the atom is not needed for the module. - if (!atom_needed_for_module(*atom, moduleName)) { - continue; - } - for (vector<AtomField>::const_iterator field = atom->fields.begin(); - field != atom->fields.end(); field++) { + for (AtomDeclSet::const_iterator atomIt = atoms.decls.begin(); atomIt != atoms.decls.end(); + atomIt++) { + for (vector<AtomField>::const_iterator field = (*atomIt)->fields.begin(); + field != (*atomIt)->fields.end(); field++) { if (field->javaType == JAVA_TYPE_ENUM) { - fprintf(out, " // Values for %s.%s\n", atom->message.c_str(), - field->name.c_str()); + fprintf(out, " // Values for %s.%s\n", (*atomIt)->message.c_str(), + field->name.c_str()); for (map<int, string>::const_iterator value = field->enumValues.begin(); - value != field->enumValues.end(); value++) { - if (moduleName == DEFAULT_MODULE_NAME) { - fprintf(out, " /** @hide */\n"); - } + value != field->enumValues.end(); value++) { fprintf(out, " public static final int %s__%s__%s = %d;\n", - make_constant_name(atom->message).c_str(), - make_constant_name(field->name).c_str(), - make_constant_name(value->second).c_str(), - value->first); + make_constant_name((*atomIt)->message).c_str(), + make_constant_name(field->name).c_str(), + make_constant_name(value->second).c_str(), value->first); } fprintf(out, "\n"); } @@ -326,11 +289,11 @@ void write_java_enum_values(FILE* out, const Atoms& atoms, const string& moduleN } void write_java_usage(FILE* out, const string& method_name, const string& atom_code_name, - const AtomDecl& atom) { - fprintf(out, " * Usage: StatsLog.%s(StatsLog.%s", - method_name.c_str(), atom_code_name.c_str()); - for (vector<AtomField>::const_iterator field = atom.fields.begin(); - field != atom.fields.end(); field++) { + const AtomDecl& atom) { + fprintf(out, " * Usage: StatsLog.%s(StatsLog.%s", method_name.c_str(), + atom_code_name.c_str()); + for (vector<AtomField>::const_iterator field = atom.fields.begin(); field != atom.fields.end(); + field++) { if (field->javaType == JAVA_TYPE_ATTRIBUTION_CHAIN) { fprintf(out, ", android.os.WorkSource workSource"); } else if (field->javaType == JAVA_TYPE_KEY_VALUE_PAIR) { @@ -344,32 +307,20 @@ void write_java_usage(FILE* out, const string& method_name, const string& atom_c fprintf(out, ");<br>\n"); } -int write_java_non_chained_methods( - FILE* out, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const string& moduleName - ) { - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - // Skip if this signature is not needed for the module. - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { - continue; - } - +int write_java_non_chained_methods(FILE* out, const SignatureInfoMap& signatureInfoMap) { + for (auto signatureInfoMapIt = signatureInfoMap.begin(); + signatureInfoMapIt != signatureInfoMap.end(); signatureInfoMapIt++) { // Print method signature. - if (DEFAULT_MODULE_NAME == moduleName) { - fprintf(out, " /** @hide */\n"); - } fprintf(out, " public static void write_non_chained(int code"); - vector<java_type_t> signature = signature_to_modules_it->first; + vector<java_type_t> signature = signatureInfoMapIt->first; int argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { - // Non chained signatures should not have attribution chains. + fprintf(stderr, "Non chained signatures should not have attribution chains.\n"); return 1; } else if (*arg == JAVA_TYPE_KEY_VALUE_PAIR) { - // Module logging does not yet support key value pair. + fprintf(stderr, "Module logging does not yet support key value pair.\n"); return 1; } else { fprintf(out, ", %s arg%d", java_type_name(*arg), argIndex); @@ -380,8 +331,8 @@ int write_java_non_chained_methods( fprintf(out, " write(code"); argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { // First two args are uid and tag of attribution chain. if (argIndex == 1) { fprintf(out, ", new int[] {arg%d}", argIndex); @@ -399,30 +350,24 @@ int write_java_non_chained_methods( return 0; } -int write_java_work_source_methods( - FILE* out, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const string& moduleName - ) { +int write_java_work_source_methods(FILE* out, const SignatureInfoMap& signatureInfoMap) { fprintf(out, " // WorkSource methods.\n"); - for (auto signature_to_modules_it = signatures_to_modules.begin(); - signature_to_modules_it != signatures_to_modules.end(); signature_to_modules_it++) { - // Skip if this signature is not needed for the module. - if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { - continue; - } - vector<java_type_t> signature = signature_to_modules_it->first; + for (auto signatureInfoMapIt = signatureInfoMap.begin(); + signatureInfoMapIt != signatureInfoMap.end(); signatureInfoMapIt++) { + vector<java_type_t> signature = signatureInfoMapIt->first; // Determine if there is Attribution in this signature. int attributionArg = -1; int argIndexMax = 0; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { argIndexMax++; if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { if (attributionArg > -1) { fprintf(stderr, "An atom contains multiple AttributionNode fields.\n"); fprintf(stderr, "This is not supported. Aborting WorkSource method writing.\n"); - fprintf(out, "\n// Invalid for WorkSource: more than one attribution chain.\n"); + fprintf(out, + "\n// Invalid for WorkSource: more than one attribution " + "chain.\n"); return 1; } attributionArg = argIndexMax; @@ -434,13 +379,10 @@ int write_java_work_source_methods( fprintf(out, "\n"); // Method header (signature) - if (DEFAULT_MODULE_NAME == moduleName) { - fprintf(out, " /** @hide */\n"); - } fprintf(out, " public static void write(int code"); int argIndex = 1; - for (vector<java_type_t>::const_iterator arg = signature.begin(); - arg != signature.end(); arg++) { + for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); + arg++) { if (*arg == JAVA_TYPE_ATTRIBUTION_CHAIN) { fprintf(out, ", android.os.WorkSource ws"); } else { @@ -450,36 +392,40 @@ int write_java_work_source_methods( } fprintf(out, ") {\n"); - // write_non_chained() component. TODO: Remove when flat uids are no longer needed. + // write_non_chained() component. TODO: Remove when flat uids are no longer + // needed. fprintf(out, " for (int i = 0; i < ws.size(); ++i) {\n"); fprintf(out, " write_non_chained(code"); for (int argIndex = 1; argIndex <= argIndexMax; argIndex++) { if (argIndex == attributionArg) { - fprintf(out, ", ws.get(i), ws.getName(i)"); + fprintf(out, ", ws.getUid(i), ws.getPackageName(i)"); } else { - fprintf(out, ", arg%d", argIndex); + fprintf(out, ", arg%d", argIndex); } } fprintf(out, ");\n"); - fprintf(out, " }\n"); // close for-loop + fprintf(out, " }\n"); // close for-loop // write() component. - fprintf(out, " java.util.ArrayList<android.os.WorkSource.WorkChain> workChains = " + fprintf(out, + " java.util.List<android.os.WorkSource.WorkChain> workChains = " "ws.getWorkChains();\n"); fprintf(out, " if (workChains != null) {\n"); - fprintf(out, " for (android.os.WorkSource.WorkChain wc : workChains) {\n"); + fprintf(out, + " for (android.os.WorkSource.WorkChain wc : workChains) " + "{\n"); fprintf(out, " write(code"); for (int argIndex = 1; argIndex <= argIndexMax; argIndex++) { if (argIndex == attributionArg) { fprintf(out, ", wc.getUids(), wc.getTags()"); } else { - fprintf(out, ", arg%d", argIndex); + fprintf(out, ", arg%d", argIndex); } } fprintf(out, ");\n"); - fprintf(out, " }\n"); // close for-loop - fprintf(out, " }\n"); // close if - fprintf(out, " }\n"); // close method + fprintf(out, " }\n"); // close for-loop + fprintf(out, " }\n"); // close if + fprintf(out, " }\n"); // close method } return 0; } diff --git a/tools/stats_log_api_gen/utils.h b/tools/stats_log_api_gen/utils.h index 50737a68bf89..73e0cb838227 100644 --- a/tools/stats_log_api_gen/utils.h +++ b/tools/stats_log_api_gen/utils.h @@ -16,29 +16,35 @@ #pragma once -#include "Collation.h" +#include <stdio.h> +#include <string.h> #include <map> #include <set> #include <vector> -#include <stdio.h> -#include <string.h> +#include "Collation.h" namespace android { namespace stats_log_api_gen { using namespace std; -const string DEFAULT_MODULE_NAME = "DEFAULT"; const string DEFAULT_CPP_NAMESPACE = "android,util"; const string DEFAULT_CPP_HEADER_IMPORT = "statslog.h"; -const string DEFAULT_ATOMS_INFO_CPP_HEADER_IMPORT = "atoms_info.h"; -const string DEFAULT_JAVA_PACKAGE = "android.util"; -const string DEFAULT_JAVA_CLASS = "StatsLogInternal"; const int JAVA_MODULE_REQUIRES_FLOAT = 0x01; const int JAVA_MODULE_REQUIRES_ATTRIBUTION = 0x02; +const int JAVA_MODULE_REQUIRES_KEY_VALUE_PAIRS = 0x04; + +const map<AnnotationId, string> ANNOTATION_ID_CONSTANTS = { + {ANNOTATION_ID_IS_UID, "ANNOTATION_ID_IS_UID"}, + {ANNOTATION_ID_TRUNCATE_TIMESTAMP, "ANNOTATION_ID_TRUNCATE_TIMESTAMP"}, + {ANNOTATION_ID_PRIMARY_FIELD, "ANNOTATION_ID_PRIMARY_FIELD"}, + {ANNOTATION_ID_PRIMARY_FIELD_FIRST_UID, "ANNOTATION_ID_PRIMARY_FIELD_FIRST_UID"}, + {ANNOTATION_ID_EXCLUSIVE_STATE, "ANNOTATION_ID_EXCLUSIVE_STATE"}, + {ANNOTATION_ID_TRIGGER_STATE_RESET, "ANNOTATION_ID_TRIGGER_STATE_RESET"}, + {ANNOTATION_ID_STATE_NESTED, "ANNOTATION_ID_STATE_NESTED"}}; string make_constant_name(const string& str); @@ -46,41 +52,32 @@ const char* cpp_type_name(java_type_t type); const char* java_type_name(java_type_t type); -bool atom_needed_for_module(const AtomDecl& atomDecl, const string& moduleName); - -bool signature_needed_for_module(const set<string>& modules, const string& moduleName); - // Common Native helpers void write_namespace(FILE* out, const string& cppNamespaces); void write_closing_namespace(FILE* out, const string& cppNamespaces); -void write_native_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, - const string& moduleName); +void write_native_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl); void write_native_method_signature(FILE* out, const string& methodName, - const vector<java_type_t>& signature, const AtomDecl& attributionDecl, - const string& closer); + const vector<java_type_t>& signature, + const AtomDecl& attributionDecl, const string& closer); void write_native_method_call(FILE* out, const string& methodName, - const vector<java_type_t>& signature, const AtomDecl& attributionDecl, int argIndex = 1); + const vector<java_type_t>& signature, const AtomDecl& attributionDecl, + int argIndex = 1); // Common Java helpers. -void write_java_atom_codes(FILE* out, const Atoms& atoms, const string& moduleName); +void write_java_atom_codes(FILE* out, const Atoms& atoms); -void write_java_enum_values(FILE* out, const Atoms& atoms, const string& moduleName); +void write_java_enum_values(FILE* out, const Atoms& atoms); void write_java_usage(FILE* out, const string& method_name, const string& atom_code_name, - const AtomDecl& atom); + const AtomDecl& atom); -int write_java_non_chained_methods(FILE* out, const map<vector<java_type_t>, - set<string>>& signatures_to_modules, - const string& moduleName); +int write_java_non_chained_methods(FILE* out, const SignatureInfoMap& signatureInfoMap); -int write_java_work_source_methods( - FILE* out, - const map<vector<java_type_t>, set<string>>& signatures_to_modules, - const string& moduleName); +int write_java_work_source_methods(FILE* out, const SignatureInfoMap& signatureInfoMap); } // namespace stats_log_api_gen } // namespace android diff --git a/tools/streaming_proto/cpp/main.cpp b/tools/streaming_proto/cpp/main.cpp index d6b9d81137ac..fe9a438d81d7 100644 --- a/tools/streaming_proto/cpp/main.cpp +++ b/tools/streaming_proto/cpp/main.cpp @@ -33,13 +33,13 @@ write_enum(stringstream& text, const EnumDescriptorProto& enu, const string& ind if (GENERATE_MAPPING) { string name = make_constant_name(enu.name()); string prefix = name + "_"; - text << indent << "const int _ENUM_" << name << "_COUNT = " << N << ";" << endl; - text << indent << "const char* _ENUM_" << name << "_NAMES[" << N << "] = {" << endl; + text << indent << "static const int _ENUM_" << name << "_COUNT = " << N << ";" << endl; + text << indent << "static const char* _ENUM_" << name << "_NAMES[" << N << "] = {" << endl; for (int i=0; i<N; i++) { text << indent << INDENT << "\"" << stripPrefix(enu.value(i).name(), prefix) << "\"," << endl; } text << indent << "};" << endl; - text << indent << "const int _ENUM_" << name << "_VALUES[" << N << "] = {" << endl; + text << indent << "static const int _ENUM_" << name << "_VALUES[" << N << "] = {" << endl; for (int i=0; i<N; i++) { text << indent << INDENT << make_constant_name(enu.value(i).name()) << "," << endl; } @@ -102,13 +102,13 @@ write_message(stringstream& text, const DescriptorProto& message, const string& if (GENERATE_MAPPING) { N = message.field_size(); - text << indented << "const int _FIELD_COUNT = " << N << ";" << endl; - text << indented << "const char* _FIELD_NAMES[" << N << "] = {" << endl; + text << indented << "static const int _FIELD_COUNT = " << N << ";" << endl; + text << indented << "static const char* _FIELD_NAMES[" << N << "] = {" << endl; for (int i=0; i<N; i++) { text << indented << INDENT << "\"" << message.field(i).name() << "\"," << endl; } text << indented << "};" << endl; - text << indented << "const uint64_t _FIELD_IDS[" << N << "] = {" << endl; + text << indented << "static const uint64_t _FIELD_IDS[" << N << "] = {" << endl; for (int i=0; i<N; i++) { text << indented << INDENT << make_constant_name(message.field(i).name()) << "," << endl; } @@ -152,7 +152,7 @@ write_header_file(CodeGeneratorResponse* response, const FileDescriptorProto& fi write_message(text, file_descriptor.message_type(i), ""); } - for (vector<string>::iterator it = namespaces.begin(); it != namespaces.end(); it++) { + for (vector<string>::reverse_iterator it = namespaces.rbegin(); it != namespaces.rend(); it++) { text << "} // " << *it << endl; } diff --git a/tools/validatekeymaps/Main.cpp b/tools/validatekeymaps/Main.cpp index 9c42724f060f..877715a66f6d 100644 --- a/tools/validatekeymaps/Main.cpp +++ b/tools/validatekeymaps/Main.cpp @@ -137,7 +137,6 @@ static bool validateFile(const char* filename) { } } - log("No errors.\n\n"); return true; } diff --git a/tools/validatekeymaps/OWNERS b/tools/validatekeymaps/OWNERS new file mode 100644 index 000000000000..0313a40f7270 --- /dev/null +++ b/tools/validatekeymaps/OWNERS @@ -0,0 +1,2 @@ +michaelwr@google.com +svv@google.com |