diff options
Diffstat (limited to 'tools')
145 files changed, 8791 insertions, 520 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/aapt2/Android.bp b/tools/aapt2/Android.bp index 53372bff3e67..6f442300bce7 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -56,7 +56,7 @@ cc_defaults { "libziparchive", "libpng", "libbase", - "libprotobuf-cpp-lite", + "libprotobuf-cpp-full", "libz", "libbuildversion", ], @@ -197,6 +197,7 @@ cc_test_host { cc_binary_host { name: "aapt2", srcs: ["Main.cpp"] + toolSources, + use_version_lib: true, static_libs: ["libaapt2"], defaults: ["aapt2_defaults"], } @@ -209,6 +210,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..137fbd671865 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -178,19 +178,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 { @@ -248,6 +246,36 @@ class ValueBodyPrinter : public ConstValueVisitor { Printer* printer_; }; +std::string OverlayablePoliciesToString(OverlayableItem::PolicyFlags policies) { + static const std::map<OverlayableItem::PolicyFlags, std::string> kFlagToString = { + {OverlayableItem::kPublic, "public"}, + {OverlayableItem::kSystem, "system"}, + {OverlayableItem::kVendor, "vendor"}, + {OverlayableItem::kProduct, "product"}, + {OverlayableItem::kSignature, "signature"}, + {OverlayableItem::kOdm, "odm"}, + {OverlayableItem::kOem, "oem"}, + }; + std::string str; + for (auto const& policy : kFlagToString) { + if ((policies & policy.first) != policy.first) { + continue; + } + if (!str.empty()) { + str.append("|"); + } + str.append(policy.second); + policies &= ~policy.first; + } + if (policies != 0) { + if (!str.empty()) { + str.append("|"); + } + str.append(StringPrintf("0x%08x", policies)); + } + return !str.empty() ? str : "none"; +} + } // namespace void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options, @@ -314,6 +342,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 +559,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")", + OverlayablePoliciesToString(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 40eae545c06b..74e2a0987c3f 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -769,16 +769,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 {}; } @@ -1389,7 +1387,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..24531bc16445 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -401,7 +401,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 +414,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) { 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..30ba1aed25f8 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -228,13 +228,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/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..8a2f5afa7255 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 { @@ -270,6 +269,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 +294,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 +395,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/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 f354bb610224..5b6935bafe71 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; @@ -1277,7 +1297,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()) { @@ -1547,16 +1568,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; @@ -1565,6 +1577,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, @@ -1578,6 +1677,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; @@ -1590,9 +1697,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; @@ -1687,6 +1794,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") { @@ -1702,6 +1810,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); @@ -1816,7 +1926,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; } @@ -1968,7 +2078,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; @@ -2102,6 +2212,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 7c583858ee1d..4722358be8f7 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; // Java/Proguard options. @@ -54,6 +56,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; @@ -70,6 +73,7 @@ struct LinkOptions { // Static lib options. bool no_static_lib_packages = false; + bool merge_only = false; // AndroidManifest.xml massaging options. ManifestFixerOptions manifest_fixer_options; @@ -79,6 +83,7 @@ struct LinkOptions { // Flattening options. TableFlattenerOptions table_flattener_options; + SerializeTableOptions proto_table_flattener_options; bool keep_raw_values = false; // Split APK options. @@ -211,6 +216,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", @@ -242,13 +250,17 @@ 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-instrumentation-target-package", "Changes the name of the target package for instrumentation. Most useful\n" "when used in conjunction with --rename-manifest-package.", &options_.manifest_fixer_options.rename_instrumentation_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); @@ -256,8 +268,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.", @@ -277,9 +289,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..e7a82034c77a 100644 --- a/tools/aapt2/dump/DumpManifest.cpp +++ b/tools/aapt2/dump/DumpManifest.cpp @@ -291,7 +291,10 @@ class ManifestExtractor { } } } - return &attr->value; + + if (!attr->value.empty()) { + return &attr->value; + } } return nullptr; } @@ -425,6 +428,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 +445,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 +468,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)); 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..fcd6aaafba7a 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -614,6 +614,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..cbce8a59bae3 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); } } @@ -228,14 +240,15 @@ 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) { @@ -652,11 +665,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 +726,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 +774,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..af2293f0f82b 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -431,6 +431,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 +559,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 +613,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 +632,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)); 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..4cd6e930915d 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -634,6 +634,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 +709,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..d9f6c193fc2f 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -290,8 +290,10 @@ 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(); @@ -319,14 +321,17 @@ static void SerializeOverlayableItemToPb(const OverlayableItem& overlayable_item pb_overlayable_item->add_policy(pb::OverlayableItem::OEM); } - 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 +361,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 +418,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 +568,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..61a8335e17a7 100644 --- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp @@ -80,7 +80,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 +133,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()); @@ -606,4 +608,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/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp index 31d205e1b9c9..bb541fe2490b 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;"); } for (const auto& package : table_->packages) { 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 806f4e37e22a..0db1807c75d9 100644 --- a/tools/aapt2/java/ProguardRules.cpp +++ b/tools/aapt2/java/ProguardRules.cpp @@ -160,13 +160,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"); } } } @@ -393,11 +399,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>(); }"); } @@ -414,7 +424,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("; }"); @@ -424,8 +436,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>("); @@ -436,8 +450,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 25b55ab003b0..b6e76021ccc1 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; } @@ -326,6 +326,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 49909f6e2b8e..5aa32f868104 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. @@ -262,6 +289,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"); @@ -320,6 +349,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, } return true; }); + manifest_action["uses-sdk"]["extension-sdk"]; // Instrumentation actions. manifest_action["instrumentation"].Action(RequiredNameIsJavaClassName); @@ -336,6 +366,8 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, }); manifest_action["instrumentation"]["meta-data"] = meta_data_action; + manifest_action["feature"]; + manifest_action["feature"]["inherit-from"]; manifest_action["original-package"]; manifest_action["overlay"]; manifest_action["protected-broadcast"]; @@ -357,6 +389,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"]; @@ -393,6 +429,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 3f1ee36dea4a..3af06f53d4f3 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -727,8 +727,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"}; @@ -736,6 +735,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")); @@ -782,6 +787,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..0be4ccf9ae85 100644 --- a/tools/aapt2/link/TableMerger_test.cpp +++ b/tools/aapt2/link/TableMerger_test.cpp @@ -352,62 +352,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,6 +436,53 @@ 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"); 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..c7c80bab67bf --- /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) + 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..ebfbbd8163b5 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt @@ -0,0 +1,220 @@ +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 annotatedTypeForSetterParam by lazy { + (annotationsNoInternal + SetterParamType).joinToString(" ") + } + + // 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..8fe243ff68cb --- /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 = annotationsNoInternal.joinToString(" "), + 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..c830aaa0df3d --- /dev/null +++ b/tools/codegen/src/com/android/codegen/ImportsProvider.kt @@ -0,0 +1,91 @@ +/* + * 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 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..74c86f4551f8 --- /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.14" + +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 91f875ed9918..ded4b916c452 100644 --- a/tools/incident_section_gen/main.cpp +++ b/tools/incident_section_gen/main.cpp @@ -436,7 +436,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()); 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..d1a86c245dec --- /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", + "protolog-proto", + "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/Android.bp b/tools/stats_log_api_gen/Android.bp index d3958a65c704..a251c053e004 100644 --- a/tools/stats_log_api_gen/Android.bp +++ b/tools/stats_log_api_gen/Android.bp @@ -30,7 +30,7 @@ cc_binary_host { "utils.cpp", ], cflags: [ - "-DSTATS_SCHEMA_LEGACY", + //"-DSTATS_SCHEMA_LEGACY", "-Wall", "-Werror", ], diff --git a/tools/stats_log_api_gen/Collation.cpp b/tools/stats_log_api_gen/Collation.cpp index 75deb017e41b..8bccd7150050 100644 --- a/tools/stats_log_api_gen/Collation.cpp +++ b/tools/stats_log_api_gen/Collation.cpp @@ -40,18 +40,21 @@ AtomDecl::AtomDecl() { } -AtomDecl::AtomDecl(const AtomDecl& that) - : code(that.code), - name(that.name), - message(that.message), - fields(that.fields), - primaryFields(that.primaryFields), - exclusiveField(that.exclusiveField), - uidField(that.uidField), - whitelisted(that.whitelisted), - binaryFields(that.binaryFields), - hasModule(that.hasModule), - moduleName(that.moduleName) {} +AtomDecl::AtomDecl(const AtomDecl &that) + : code(that.code), + name(that.name), + message(that.message), + fields(that.fields), + primaryFields(that.primaryFields), + exclusiveField(that.exclusiveField), + defaultState(that.defaultState), + resetState(that.resetState), + nested(that.nested), + 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), @@ -281,7 +284,7 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, atomDecl->fields.push_back(atField); if (field->options().GetExtension(os::statsd::state_field_option).option() == - os::statsd::StateField::PRIMARY) { + os::statsd::StateField::PRIMARY_FIELD) { if (javaType == JAVA_TYPE_UNKNOWN || javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || javaType == JAVA_TYPE_OBJECT || javaType == JAVA_TYPE_BYTE_ARRAY) { @@ -291,7 +294,16 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, } if (field->options().GetExtension(os::statsd::state_field_option).option() == - os::statsd::StateField::EXCLUSIVE) { + os::statsd::StateField::PRIMARY_FIELD_FIRST_UID) { + if (javaType != JAVA_TYPE_ATTRIBUTION_CHAIN) { + errorCount++; + } else { + atomDecl->primaryFields.push_back(FIRST_UID_IN_CHAIN_ID); + } + } + + if (field->options().GetExtension(os::statsd::state_field_option).option() == + os::statsd::StateField::EXCLUSIVE_STATE) { if (javaType == JAVA_TYPE_UNKNOWN || javaType == JAVA_TYPE_ATTRIBUTION_CHAIN || javaType == JAVA_TYPE_OBJECT || javaType == JAVA_TYPE_BYTE_ARRAY) { @@ -303,6 +315,21 @@ int collate_atom(const Descriptor *atom, AtomDecl *atomDecl, } else { errorCount++; } + + if (field->options() + .GetExtension(os::statsd::state_field_option) + .has_default_state_value()) { + atomDecl->defaultState = field->options() + .GetExtension(os::statsd::state_field_option) + .default_state_value(); + } + + if (field->options().GetExtension(os::statsd::state_field_option).has_reset_state_value()) { + atomDecl->resetState = field->options() + .GetExtension(os::statsd::state_field_option) + .reset_state_value(); + } + atomDecl->nested = field->options().GetExtension(os::statsd::state_field_option).nested(); } if (field->options().GetExtension(os::statsd::is_uid) == true) { @@ -405,9 +432,9 @@ int collate_atoms(const Descriptor *descriptor, Atoms *atoms) { atomDecl.whitelisted = true; } - if (atomField->options().HasExtension(os::statsd::log_from_module)) { + if (atomField->options().HasExtension(os::statsd::module)) { atomDecl.hasModule = true; - atomDecl.moduleName = atomField->options().GetExtension(os::statsd::log_from_module); + atomDecl.moduleName = atomField->options().GetExtension(os::statsd::module); } vector<java_type_t> signature; diff --git a/tools/stats_log_api_gen/Collation.h b/tools/stats_log_api_gen/Collation.h index 3efdd520d7f5..65d8e3efee5b 100644 --- a/tools/stats_log_api_gen/Collation.h +++ b/tools/stats_log_api_gen/Collation.h @@ -36,6 +36,8 @@ using google::protobuf::FieldDescriptor; const int PULL_ATOM_START_ID = 10000; +const int FIRST_UID_IN_CHAIN_ID = 0; + /** * The types for atom parameters. */ @@ -86,6 +88,9 @@ struct AtomDecl { vector<int> primaryFields; int exclusiveField = 0; + int defaultState = INT_MAX; + int resetState = INT_MAX; + bool nested; int uidField = 0; diff --git a/tools/stats_log_api_gen/atoms_info_writer.cpp b/tools/stats_log_api_gen/atoms_info_writer.cpp index 54a9982bb5c2..984c929f63bd 100644 --- a/tools/stats_log_api_gen/atoms_info_writer.cpp +++ b/tools/stats_log_api_gen/atoms_info_writer.cpp @@ -25,9 +25,15 @@ namespace android { namespace stats_log_api_gen { static void write_atoms_info_header_body(FILE* out, const Atoms& atoms) { + fprintf(out, "static int UNSET_VALUE = INT_MAX;\n"); + fprintf(out, "static int FIRST_UID_IN_CHAIN = 0;\n"); + fprintf(out, "struct StateAtomFieldOptions {\n"); fprintf(out, " std::vector<int> primaryFields;\n"); fprintf(out, " int exclusiveField;\n"); + fprintf(out, " int defaultState = UNSET_VALUE;\n"); + fprintf(out, " int resetState = UNSET_VALUE;\n"); + fprintf(out, " bool nested;\n"); fprintf(out, "};\n"); fprintf(out, "\n"); @@ -124,7 +130,7 @@ static void write_atoms_info_cpp_body(FILE* out, const Atoms& atoms) { "static std::map<int, StateAtomFieldOptions> " "getStateAtomFieldOptions() {\n"); fprintf(out, " std::map<int, StateAtomFieldOptions> options;\n"); - fprintf(out, " StateAtomFieldOptions opt;\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) { @@ -134,14 +140,26 @@ static void write_atoms_info_cpp_body(FILE* out, const Atoms& atoms) { "\n // Adding primary and exclusive fields for atom " "(%d)%s\n", atom->code, atom->name.c_str()); - fprintf(out, " opt.primaryFields.clear();\n"); + fprintf(out, " opt = &(options[static_cast<int>(%s)]);\n", + make_constant_name(atom->name).c_str()); + fprintf(out, " opt->primaryFields.reserve(%lu);\n", atom->primaryFields.size()); for (const auto& field : atom->primaryFields) { - fprintf(out, " opt.primaryFields.push_back(%d);\n", field); + 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, " opt->exclusiveField = %d;\n", atom->exclusiveField); + if (atom->defaultState != INT_MAX) { + fprintf(out, " opt->defaultState = %d;\n", atom->defaultState); + } else { + fprintf(out, " opt->defaultState = UNSET_VALUE;\n"); + } + + if (atom->resetState != INT_MAX) { + fprintf(out, " opt->resetState = %d;\n", atom->resetState); + } else { + fprintf(out, " opt->resetState = UNSET_VALUE;\n"); + } + fprintf(out, " opt->nested = %d;\n", atom->nested); } fprintf(out, " return options;\n"); diff --git a/tools/stats_log_api_gen/java_writer_q.cpp b/tools/stats_log_api_gen/java_writer_q.cpp index db766b2ade71..12c050d8ef8d 100644 --- a/tools/stats_log_api_gen/java_writer_q.cpp +++ b/tools/stats_log_api_gen/java_writer_q.cpp @@ -75,9 +75,7 @@ int write_java_methods_q_schema( 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); } @@ -161,9 +159,112 @@ int write_java_methods_q_schema( 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, KEY_VALUE_PAIR. - fprintf(stderr, "Module logging does not yet support key value pair.\n"); + // Unsupported types: OBJECT, DOUBLE. + fprintf(stderr, "Module logging does not yet support Object and Double.\n"); return 1; } argIndex++; @@ -253,10 +354,19 @@ int write_java_methods_q_schema( 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, KEY_VALUE_PAIR. + // Unsupported types: OBJECT, DOUBLE. fprintf(stderr, - "Object, Double, and KeyValuePairs are not supported in module logging"); + "Object and Double are not supported in module logging"); return 1; } argIndex++; @@ -359,6 +469,100 @@ void write_java_helpers_for_q_schema_methods( 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 @@ -426,7 +630,7 @@ static void write_java_method( 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"); + fprintf(out, ", android.util.SparseArray<Object> valueMap"); } else { fprintf(out, ", %s arg%d", java_type_name(*arg), argIndex); } diff --git a/tools/stats_log_api_gen/native_writer.cpp b/tools/stats_log_api_gen/native_writer.cpp index c7a34feff94b..285514df5ff3 100644 --- a/tools/stats_log_api_gen/native_writer.cpp +++ b/tools/stats_log_api_gen/native_writer.cpp @@ -22,15 +22,6 @@ 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 int write_native_stats_write_methods(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, const string& moduleName, const bool supportQ) { @@ -41,7 +32,10 @@ static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, continue; } vector<java_type_t> signature = signature_to_modules_it->first; - + // Key value pairs not supported in native. + if (find(signature.begin(), signature.end(), JAVA_TYPE_KEY_VALUE_PAIR) != signature.end()) { + continue; + } write_native_method_signature(out, "int stats_write", signature, attributionDecl, " {"); @@ -59,11 +53,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); @@ -85,7 +74,7 @@ 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; } @@ -93,8 +82,8 @@ static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, } 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"); for (vector<java_type_t>::const_iterator arg = signature.begin(); arg != signature.end(); arg++) { switch (*arg) { @@ -102,57 +91,43 @@ static int write_native_stats_write_methods(FILE* out, const Atoms& atoms, 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, " + " 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_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; } 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"); @@ -169,6 +144,10 @@ static void write_native_stats_write_non_chained_methods(FILE* out, const Atoms& continue; } 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; + } write_native_method_signature(out, "int stats_write_non_chained", signature, attributionDecl, " {"); @@ -210,8 +189,14 @@ static void write_native_method_header( if (!signature_needed_for_module(signature_to_modules_it->second, moduleName)) { continue; } - vector<java_type_t> signature = signature_to_modules_it->first; + +#if !defined(STATS_SCHEMA_LEGACY) + // Key value pairs not supported in native. + if (find(signature.begin(), signature.end(), JAVA_TYPE_KEY_VALUE_PAIR) != signature.end()) { + continue; + } +#endif write_native_method_signature(out, methodName, signature, attributionDecl, ";"); } } diff --git a/tools/stats_log_api_gen/test.proto b/tools/stats_log_api_gen/test.proto index c3e703826be5..b892194410ae 100644 --- a/tools/stats_log_api_gen/test.proto +++ b/tools/stats_log_api_gen/test.proto @@ -148,53 +148,42 @@ 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).option = PRIMARY_FIELD]; } // 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).option = PRIMARY_FIELD]; + optional int32 state = 2 [(android.os.statsd.state_field_option).option = EXCLUSIVE_STATE]; } // 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).option = PRIMARY_FIELD]; + optional int32 state = 2 [(android.os.statsd.state_field_option).option = EXCLUSIVE_STATE]; + optional int32 state2 = 3 [(android.os.statsd.state_field_option).option = EXCLUSIVE_STATE]; } 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).option = PRIMARY_FIELD]; + optional int32 state = 2 [(android.os.statsd.state_field_option).option = EXCLUSIVE_STATE]; } // 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).option = EXCLUSIVE_STATE]; } // 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]; + optional int32 uid = 1 [(android.os.statsd.state_field_option).option = PRIMARY_FIELD]; + optional int32 tid = 2 [(android.os.statsd.state_field_option).option = PRIMARY_FIELD]; + optional int32 state = 3 [(android.os.statsd.state_field_option).option = EXCLUSIVE_STATE]; } message WhitelistedAtom { @@ -229,8 +218,8 @@ message NoModuleAtom { 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"]; + ModuleOneAtom module_one_atom = 1 [(android.os.statsd.module) = "module1"]; + ModuleTwoAtom module_two_atom = 2 [(android.os.statsd.module) = "module2"]; NoModuleAtom no_module_atom = 3; } -}
\ No newline at end of file +} diff --git a/tools/stats_log_api_gen/utils.cpp b/tools/stats_log_api_gen/utils.cpp index 641404280093..8c4abe43a49b 100644 --- a/tools/stats_log_api_gen/utils.cpp +++ b/tools/stats_log_api_gen/utils.cpp @@ -455,7 +455,7 @@ int write_java_work_source_methods( 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); } @@ -464,7 +464,7 @@ int write_java_work_source_methods( 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"); diff --git a/tools/stats_log_api_gen/utils.h b/tools/stats_log_api_gen/utils.h index 50737a68bf89..cd602e53359a 100644 --- a/tools/stats_log_api_gen/utils.h +++ b/tools/stats_log_api_gen/utils.h @@ -39,6 +39,7 @@ 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; string make_constant_name(const string& str); 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 56a242f1daaf..5ac9dfd2a557 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; } |