diff options
Diffstat (limited to 'tools')
65 files changed, 2814 insertions, 162 deletions
diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp index 5e0f87f0dcaf..60c4bf5c4131 100644 --- a/tools/aapt/Package.cpp +++ b/tools/aapt/Package.cpp @@ -292,13 +292,12 @@ bool processFile(Bundle* bundle, ZipFile* zip, } if (!hasData) { const String8& srcName = file->getSourceFile(); - time_t fileModWhen; - fileModWhen = getFileModDate(srcName.c_str()); - if (fileModWhen == (time_t) -1) { // file existence tested earlier, - return false; // not expecting an error here + auto fileModWhen = getFileModDate(srcName.c_str()); + if (fileModWhen == kInvalidModDate) { // file existence tested earlier, + return false; // not expecting an error here } - - if (fileModWhen > entry->getModWhen()) { + + if (toTimeT(fileModWhen) > entry->getModWhen()) { // mark as deleted so add() will succeed if (bundle->getVerbose()) { printf(" (removing old '%s')\n", storageName.c_str()); diff --git a/tools/aapt/StringPool.cpp b/tools/aapt/StringPool.cpp index 1af8d6f67bd3..b2e48bd74e8a 100644 --- a/tools/aapt/StringPool.cpp +++ b/tools/aapt/StringPool.cpp @@ -40,7 +40,7 @@ void strcpy16_htod(uint16_t* dst, const char16_t* src) void printStringPool(const ResStringPool* pool) { if (pool->getError() == NO_INIT) { - printf("String pool is unitialized.\n"); + printf("String pool is uninitialized.\n"); return; } else if (pool->getError() != NO_ERROR) { printf("String pool is corrupt/invalid.\n"); diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index df1d51e37660..e24fe07f959b 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -21,10 +21,13 @@ #include <format/binary/ResChunkPullParser.h> #include <algorithm> +#include <array> #include <map> #include <memory> #include <queue> #include <set> +#include <span> +#include <utility> #include <vector> #include "ResourceTable.h" @@ -346,6 +349,21 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& value->value->Accept(&body_printer); printer->Undent(); } + printer->Println("Flag disabled values:"); + for (const auto& value : entry.flag_disabled_values) { + printer->Print("("); + printer->Print(value->config.to_string()); + printer->Print(") "); + value->value->Accept(&headline_printer); + if (options.show_sources && !value->value->GetSource().path.empty()) { + printer->Print(" src="); + printer->Print(value->value->GetSource().to_string()); + } + printer->Println(); + printer->Indent(); + value->value->Accept(&body_printer); + printer->Undent(); + } printer->Undent(); } } @@ -430,7 +448,7 @@ void Debug::DumpResStringPool(const android::ResStringPool* pool, text::Printer* using namespace android; if (pool->getError() == NO_INIT) { - printer->Print("String pool is unitialized.\n"); + printer->Print("String pool is uninitialized.\n"); return; } else if (pool->getError() != NO_ERROR) { printer->Print("String pool is corrupt/invalid.\n"); @@ -665,17 +683,91 @@ class ChunkPrinter { item->PrettyPrint(printer_); printer_->Print(")"); } + } - printer_->Print("\n"); + void PrintQualifiers(uint32_t qualifiers) const { + if (qualifiers == 0) { + printer_->Print("0"); + return; + } + + printer_->Print(StringPrintf("0x%04x: ", qualifiers)); + static constinit std::array kValues = { + std::pair{ResTable_config::CONFIG_MCC, "mcc"}, + std::pair{ResTable_config::CONFIG_MNC, "mnc"}, + std::pair{ResTable_config::CONFIG_LOCALE, "locale"}, + std::pair{ResTable_config::CONFIG_TOUCHSCREEN, "touchscreen"}, + std::pair{ResTable_config::CONFIG_KEYBOARD, "keyboard"}, + std::pair{ResTable_config::CONFIG_KEYBOARD_HIDDEN, "keyboard_hidden"}, + std::pair{ResTable_config::CONFIG_NAVIGATION, "navigation"}, + std::pair{ResTable_config::CONFIG_ORIENTATION, "orientation"}, + std::pair{ResTable_config::CONFIG_DENSITY, "screen_density"}, + std::pair{ResTable_config::CONFIG_SCREEN_SIZE, "screen_size"}, + std::pair{ResTable_config::CONFIG_SMALLEST_SCREEN_SIZE, "screen_smallest_size"}, + std::pair{ResTable_config::CONFIG_VERSION, "version"}, + std::pair{ResTable_config::CONFIG_SCREEN_LAYOUT, "screen_layout"}, + std::pair{ResTable_config::CONFIG_UI_MODE, "ui_mode"}, + std::pair{ResTable_config::CONFIG_LAYOUTDIR, "layout_dir"}, + std::pair{ResTable_config::CONFIG_SCREEN_ROUND, "screen_round"}, + std::pair{ResTable_config::CONFIG_COLOR_MODE, "color_mode"}, + std::pair{ResTable_config::CONFIG_GRAMMATICAL_GENDER, "grammatical_gender"}}; + const char* delimiter = ""; + for (auto&& pair : kValues) { + if (qualifiers & pair.first) { + printer_->Print(StringPrintf("%s%s", delimiter, pair.second)); + delimiter = "|"; + } + } + } + + bool PrintTypeSpec(const ResTable_typeSpec* chunk) const { + printer_->Print(StringPrintf(" id: 0x%02x", android::util::DeviceToHost32(chunk->id))); + printer_->Print(StringPrintf(" types: %u", android::util::DeviceToHost16(chunk->typesCount))); + printer_->Print( + StringPrintf(" entry configs: %u\n", android::util::DeviceToHost32(chunk->entryCount))); + printer_->Print("Entry qualifier masks:\n"); + printer_->Indent(); + std::span<const uint32_t> masks(reinterpret_cast<const uint32_t*>(GetChunkData(&chunk->header)), + GetChunkDataLen(&chunk->header) / sizeof(uint32_t)); + int i = 0; + int non_empty_count = 0; + for (auto dev_mask : masks) { + auto mask = android::util::DeviceToHost32(dev_mask); + if (mask == 0) { + i++; + continue; + } + ++non_empty_count; + printer_->Print(StringPrintf("#0x%02x = ", i++)); + if (mask & ResTable_typeSpec::SPEC_PUBLIC) { + mask &= ~ResTable_typeSpec::SPEC_PUBLIC; + printer_->Print("(PUBLIC) "); + } + if (mask & ResTable_typeSpec::SPEC_STAGED_API) { + mask &= ~ResTable_typeSpec::SPEC_STAGED_API; + printer_->Print("(STAGED) "); + } + PrintQualifiers(mask); + printer_->Print("\n"); + } + if (non_empty_count > 0) { + printer_->Print("\n"); + } else { + printer_->Print("(all empty)\n"); + } + printer_->Undent(); + return true; } bool PrintTableType(const ResTable_type* chunk) { printer_->Print(StringPrintf(" id: 0x%02x", android::util::DeviceToHost32(chunk->id))); - printer_->Print(StringPrintf( - " name: %s", - android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1) - .c_str())); + const auto name = + android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1); + printer_->Print(StringPrintf(" name: %s", name.c_str())); printer_->Print(StringPrintf(" flags: 0x%02x", android::util::DeviceToHost32(chunk->flags))); + printer_->Print(android::util::DeviceToHost32(chunk->flags) & ResTable_type::FLAG_SPARSE + ? " (SPARSE)" + : " (DENSE)"); printer_->Print( StringPrintf(" entryCount: %u", android::util::DeviceToHost32(chunk->entryCount))); printer_->Print( @@ -685,8 +777,7 @@ class ChunkPrinter { config.copyFromDtoH(chunk->config); printer_->Print(StringPrintf(" config: %s\n", config.to_string().c_str())); - const ResourceType* type = ParseResourceType( - android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1)); + const ResourceType* type = ParseResourceType(name); printer_->Indent(); @@ -725,11 +816,8 @@ class ChunkPrinter { for (size_t i = 0; i < map_entry_count; i++) { PrintResValue(&(maps[i].value), config, type); - printer_->Print(StringPrintf( - " name: %s name-id:%d\n", - android::util::GetString(key_pool_, android::util::DeviceToHost32(maps[i].name.ident)) - .c_str(), - android::util::DeviceToHost32(maps[i].name.ident))); + printer_->Print(StringPrintf(" name-id: 0x%08x\n", + android::util::DeviceToHost32(maps[i].name.ident))); } } else { printer_->Print("\n"); @@ -737,6 +825,8 @@ class ChunkPrinter { // Print the value of the entry Res_value value = entry->value(); PrintResValue(&value, config, type); + + printer_->Print("\n"); } printer_->Undent(); @@ -849,6 +939,10 @@ class ChunkPrinter { PrintTableType(reinterpret_cast<const ResTable_type*>(chunk)); break; + case RES_TABLE_TYPE_SPEC_TYPE: + PrintTypeSpec(reinterpret_cast<const ResTable_typeSpec*>(chunk)); + break; + default: printer_->Print("\n"); break; diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index a274f047586c..0d261abd728d 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -71,6 +71,17 @@ enum class ResourceType { enum class FlagStatus { NoFlag = 0, Disabled = 1, Enabled = 2 }; +struct FeatureFlagAttribute { + std::string name; + bool negated = false; + + std::string ToString() { + return (negated ? "!" : "") + name; + } + + bool operator==(const FeatureFlagAttribute& o) const = default; +}; + android::StringPiece to_string(ResourceType type); /** @@ -232,6 +243,12 @@ struct ResourceFile { // Exported symbols std::vector<SourcedResourceName> exported_symbols; + + // Flag status + FlagStatus flag_status = FlagStatus::NoFlag; + + // Flag + std::optional<FeatureFlagAttribute> flag; }; /** diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index a5aecc855707..fb576df248be 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -107,9 +107,10 @@ struct ParsedResource { Visibility::Level visibility_level = Visibility::Level::kUndefined; bool staged_api = false; bool allow_new = false; - FlagStatus flag_status = FlagStatus::NoFlag; std::optional<OverlayableItem> overlayable_item; std::optional<StagedId> staged_alias; + std::optional<FeatureFlagAttribute> flag; + FlagStatus flag_status = FlagStatus::NoFlag; std::string comment; std::unique_ptr<Value> value; @@ -151,6 +152,7 @@ static bool AddResourcesToTable(ResourceTable* table, android::IDiagnostics* dia } if (res->value != nullptr) { + res->value->SetFlag(res->flag); res->value->SetFlagStatus(res->flag_status); // Attach the comment, source and config to the value. res->value->SetComment(std::move(res->comment)); @@ -162,8 +164,6 @@ static bool AddResourcesToTable(ResourceTable* table, android::IDiagnostics* dia res_builder.SetStagedId(res->staged_alias.value()); } - res_builder.SetFlagStatus(res->flag_status); - bool error = false; if (!res->name.entry.empty()) { if (!table->AddResource(res_builder.Build(), diag)) { @@ -546,12 +546,26 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, {"symbol", std::mem_fn(&ResourceParser::ParseSymbol)}, }); - std::string resource_type = parser->element_name(); - auto flag_status = GetFlagStatus(parser); - if (!flag_status) { - return false; + std::string_view resource_type = parser->element_name(); + if (auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"))) { + if (options_.flag) { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) + << "Resource flag are not allowed both in the path and in the file"); + return false; + } + out_resource->flag = std::move(flag); + std::string error; + auto flag_status = GetFlagStatus(out_resource->flag, options_.feature_flag_values, &error); + if (flag_status) { + out_resource->flag_status = flag_status.value(); + } else { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << error); + return false; + } + } else if (options_.flag) { + out_resource->flag = options_.flag; + out_resource->flag_status = options_.flag_status; } - out_resource->flag_status = flag_status.value(); // The value format accepted for this resource. uint32_t resource_format = 0u; @@ -567,7 +581,7 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, // Items have their type encoded in the type attribute. if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) { - resource_type = std::string(maybe_type.value()); + resource_type = maybe_type.value(); } else { diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << "<item> must have a 'type' attribute"); @@ -590,7 +604,7 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, // Bags have their type encoded in the type attribute. if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) { - resource_type = std::string(maybe_type.value()); + resource_type = maybe_type.value(); } else { diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << "<bag> must have a 'type' attribute"); @@ -733,33 +747,6 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, return false; } -std::optional<FlagStatus> ResourceParser::GetFlagStatus(xml::XmlPullParser* parser) { - auto flag_status = FlagStatus::NoFlag; - - std::optional<StringPiece> flag = xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"); - if (flag) { - auto flag_it = options_.feature_flag_values.find(flag.value()); - if (flag_it == options_.feature_flag_values.end()) { - diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) - << "Resource flag value undefined"); - return {}; - } - const auto& flag_properties = flag_it->second; - if (!flag_properties.read_only) { - diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) - << "Only read only flags may be used with resources"); - return {}; - } - if (!flag_properties.enabled.has_value()) { - diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) - << "Only flags with a value may be used with resources"); - return {}; - } - flag_status = flag_properties.enabled.value() ? FlagStatus::Enabled : FlagStatus::Disabled; - } - return flag_status; -} - bool ResourceParser::ParseItem(xml::XmlPullParser* parser, ParsedResource* out_resource, const uint32_t format) { @@ -1542,13 +1529,34 @@ bool ResourceParser::ParseStyleItem(xml::XmlPullParser* parser, Style* style) { ResolvePackage(parser, &maybe_key.value()); maybe_key.value().SetSource(source); + auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag")); + std::unique_ptr<Item> value = ParseXml(parser, 0, kAllowRawString); if (!value) { diag_->Error(android::DiagMessage(source) << "could not parse style item"); return false; } - style->entries.push_back(Style::Entry{std::move(maybe_key.value()), std::move(value)}); + if (flag) { + if (options_.flag) { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) + << "Resource flag are not allowed both in the path and in the file"); + return false; + } + std::string error; + auto flag_status = GetFlagStatus(flag, options_.feature_flag_values, &error); + if (flag_status) { + value->SetFlagStatus(flag_status.value()); + value->SetFlag(std::move(flag)); + } else { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << error); + return false; + } + } + + if (value->GetFlagStatus() != FlagStatus::Disabled) { + style->entries.push_back(Style::Entry{std::move(maybe_key.value()), std::move(value)}); + } return true; } @@ -1666,21 +1674,25 @@ bool ResourceParser::ParseArrayImpl(xml::XmlPullParser* parser, const std::string& element_namespace = parser->element_namespace(); const std::string& element_name = parser->element_name(); if (element_namespace.empty() && element_name == "item") { - auto flag_status = GetFlagStatus(parser); - if (!flag_status) { - error = true; - continue; - } + auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag")); std::unique_ptr<Item> item = ParseXml(parser, typeMask, kNoRawString); if (!item) { diag_->Error(android::DiagMessage(item_source) << "could not parse array item"); error = true; continue; } - item->SetFlagStatus(flag_status.value()); + item->SetFlag(flag); + std::string err; + auto status = GetFlagStatus(flag, options_.feature_flag_values, &err); + if (status) { + item->SetFlagStatus(status.value()); + } else { + diag_->Error(android::DiagMessage(item_source) << err); + error = true; + continue; + } item->SetSource(item_source); array->elements.emplace_back(std::move(item)); - } else if (!ShouldIgnoreElement(element_namespace, element_name)) { diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << "unknown tag <" << element_namespace << ":" << element_name << ">"); diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h index 442dea89ef40..90690d522ef2 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -57,6 +57,11 @@ struct ResourceParserOptions { std::optional<Visibility::Level> visibility; FeatureFlagValues feature_flag_values; + + // The flag that should be applied to all resources parsed + std::optional<FeatureFlagAttribute> flag; + + FlagStatus flag_status = FlagStatus::NoFlag; }; struct FlattenedXmlSubTree { @@ -85,8 +90,6 @@ class ResourceParser { private: DISALLOW_COPY_AND_ASSIGN(ResourceParser); - std::optional<FlagStatus> GetFlagStatus(xml::XmlPullParser* parser); - std::optional<FlattenedXmlSubTree> CreateFlattenSubTree(xml::XmlPullParser* parser); // Parses the XML subtree as a StyleString (flattened XML representation for strings with diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 97514599c0b1..5435cba290fc 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -101,6 +101,21 @@ struct lt_config_key_ref { } }; +struct ConfigFlagKey { + const ConfigDescription* config; + StringPiece product; + const FeatureFlagAttribute& flag; +}; + +struct lt_config_flag_key_ref { + template <typename T> + bool operator()(const T& lhs, const ConfigFlagKey& rhs) const noexcept { + return std::tie(lhs->config, lhs->product, lhs->value->GetFlag()->name, + lhs->value->GetFlag()->negated) < + std::tie(*rhs.config, rhs.product, rhs.flag.name, rhs.flag.negated); + } +}; + } // namespace ResourceTable::ResourceTable(ResourceTable::Validation validation) : validation_(validation) { @@ -213,6 +228,25 @@ std::vector<ResourceConfigValue*> ResourceEntry::FindAllValues(const ConfigDescr return results; } +ResourceConfigValue* ResourceEntry::FindOrCreateFlagDisabledValue( + const FeatureFlagAttribute& flag, const android::ConfigDescription& config, + android::StringPiece product) { + auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(), + ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref()); + if (iter != flag_disabled_values.end()) { + ResourceConfigValue* value = iter->get(); + const auto value_flag = value->value->GetFlag().value(); + if (value_flag.name == flag.name && value_flag.negated == flag.negated && + value->config == config && value->product == product) { + return value; + } + } + ResourceConfigValue* newValue = + flag_disabled_values.insert(iter, util::make_unique<ResourceConfigValue>(config, product)) + ->get(); + return newValue; +} + bool ResourceEntry::HasDefaultValue() const { // The default config should be at the top of the list, since the list is sorted. return !values.empty() && values.front()->config == ConfigDescription::DefaultConfig(); @@ -375,13 +409,14 @@ struct EntryViewComparer { } }; -void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePackage* package, - const ResourceTableType* type, const std::string& entry_name, - const std::optional<ResourceId>& id, const Visibility& visibility, - const std::optional<AllowNew>& allow_new, - const std::optional<OverlayableItem>& overlayable_item, - const std::optional<StagedId>& staged_id, - const std::vector<std::unique_ptr<ResourceConfigValue>>& values) { +void InsertEntryIntoTableView( + ResourceTableView& table, const ResourceTablePackage* package, const ResourceTableType* type, + const std::string& entry_name, const std::optional<ResourceId>& id, + const Visibility& visibility, const std::optional<AllowNew>& allow_new, + const std::optional<OverlayableItem>& overlayable_item, + const std::optional<StagedId>& staged_id, + const std::vector<std::unique_ptr<ResourceConfigValue>>& values, + const std::vector<std::unique_ptr<ResourceConfigValue>>& flag_disabled_values) { SortedVectorInserter<ResourceTablePackageView, PackageViewComparer> package_inserter; SortedVectorInserter<ResourceTableTypeView, TypeViewComparer> type_inserter; SortedVectorInserter<ResourceTableEntryView, EntryViewComparer> entry_inserter; @@ -408,6 +443,9 @@ void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePacka for (auto& value : values) { new_entry.values.emplace_back(value.get()); } + for (auto& value : flag_disabled_values) { + new_entry.flag_disabled_values.emplace_back(value.get()); + } entry_inserter.Insert(view_type->entries, std::move(new_entry)); } @@ -426,6 +464,21 @@ const ResourceConfigValue* ResourceTableEntryView::FindValue(const ConfigDescrip return nullptr; } +const ResourceConfigValue* ResourceTableEntryView::FindFlagDisabledValue( + const FeatureFlagAttribute& flag, const ConfigDescription& config, + android::StringPiece product) const { + auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(), + ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref()); + if (iter != values.end()) { + const ResourceConfigValue* value = *iter; + if (value->value->GetFlag() == flag && value->config == config && + StringPiece(value->product) == product) { + return value; + } + } + return nullptr; +} + ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptions& options) const { ResourceTableView view; for (const auto& package : packages) { @@ -433,13 +486,13 @@ ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptio for (const auto& entry : type->entries) { InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, entry->id, entry->visibility, entry->allow_new, entry->overlayable_item, - entry->staged_id, entry->values); + entry->staged_id, entry->values, entry->flag_disabled_values); if (options.create_alias_entries && entry->staged_id) { auto alias_id = entry->staged_id.value().id; InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, alias_id, entry->visibility, entry->allow_new, entry->overlayable_item, {}, - entry->values); + entry->values, entry->flag_disabled_values); } } } @@ -587,6 +640,25 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) entry->staged_id = res.staged_id.value(); } + if (res.value != nullptr && res.value->GetFlagStatus() == FlagStatus::Disabled) { + auto disabled_config_value = + entry->FindOrCreateFlagDisabledValue(res.value->GetFlag().value(), res.config, res.product); + if (!disabled_config_value->value) { + // Resource does not exist, add it now. + // Must clone the value since it might be in the values vector as well + CloningValueTransformer cloner(&string_pool); + disabled_config_value->value = res.value->Transform(cloner); + } else { + diag->Error(android::DiagMessage(source) + << "duplicate value for resource '" << res.name << "' " << "with config '" + << res.config << "' and flag '" + << (res.value->GetFlag().value().negated ? "!" : "") + << res.value->GetFlag().value().name << "'"); + diag->Error(android::DiagMessage(source) << "resource previously defined here"); + return false; + } + } + if (res.value != nullptr) { auto config_value = entry->FindOrCreateValue(res.config, res.product); if (!config_value->value) { @@ -595,9 +667,9 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) } else { // When validation is enabled, ensure that a resource cannot have multiple values defined for // the same configuration unless protected by flags. - auto result = - validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(), res.flag_status) - : CollisionResult::kKeepBoth; + auto result = validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(), + res.value->GetFlagStatus()) + : CollisionResult::kKeepBoth; if (result == CollisionResult::kConflict) { result = ResolveValueCollision(config_value->value.get(), res.value.get()); } @@ -771,11 +843,6 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) { return *this; } -NewResourceBuilder& NewResourceBuilder::SetFlagStatus(FlagStatus flag_status) { - res_.flag_status = flag_status; - return *this; -} - NewResource NewResourceBuilder::Build() { return std::move(res_); } diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index cba6b70cfbd6..b0e185536d16 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -136,6 +136,9 @@ class ResourceEntry { // The resource's values for each configuration. std::vector<std::unique_ptr<ResourceConfigValue>> values; + // The resource's values that are behind disabled flags. + std::vector<std::unique_ptr<ResourceConfigValue>> flag_disabled_values; + explicit ResourceEntry(android::StringPiece name) : name(name) { } @@ -148,6 +151,13 @@ class ResourceEntry { android::StringPiece product); std::vector<ResourceConfigValue*> FindAllValues(const android::ConfigDescription& config); + // Either returns the existing ResourceConfigValue in the disabled list with the given flag, + // config, and product or creates a new one and returns that. In either case the returned value + // does not have the flag set on the value so it must be set by the caller. + ResourceConfigValue* FindOrCreateFlagDisabledValue(const FeatureFlagAttribute& flag, + const android::ConfigDescription& config, + android::StringPiece product = {}); + template <typename Func> std::vector<ResourceConfigValue*> FindValuesIf(Func f) { std::vector<ResourceConfigValue*> results; @@ -215,9 +225,14 @@ struct ResourceTableEntryView { std::optional<OverlayableItem> overlayable_item; std::optional<StagedId> staged_id; std::vector<const ResourceConfigValue*> values; + std::vector<const ResourceConfigValue*> flag_disabled_values; const ResourceConfigValue* FindValue(const android::ConfigDescription& config, android::StringPiece product = {}) const; + + const ResourceConfigValue* FindFlagDisabledValue(const FeatureFlagAttribute& flag, + const android::ConfigDescription& config, + android::StringPiece product = {}) const; }; struct ResourceTableTypeView { @@ -269,7 +284,6 @@ struct NewResource { std::optional<AllowNew> allow_new; std::optional<StagedId> staged_id; bool allow_mangled = false; - FlagStatus flag_status = FlagStatus::NoFlag; }; struct NewResourceBuilder { @@ -283,7 +297,6 @@ struct NewResourceBuilder { NewResourceBuilder& SetAllowNew(AllowNew allow_new); NewResourceBuilder& SetStagedId(StagedId id); NewResourceBuilder& SetAllowMangled(bool allow_mangled); - NewResourceBuilder& SetFlagStatus(FlagStatus flag_status); NewResource Build(); private: diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp index b75e87c90128..723cfc0e035b 100644 --- a/tools/aapt2/ResourceValues.cpp +++ b/tools/aapt2/ResourceValues.cpp @@ -1102,6 +1102,7 @@ template <typename T> std::unique_ptr<T> CopyValueFields(std::unique_ptr<T> new_value, const T* value) { new_value->SetSource(value->GetSource()); new_value->SetComment(value->GetComment()); + new_value->SetFlag(value->GetFlag()); new_value->SetFlagStatus(value->GetFlagStatus()); return new_value; } diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h index a1b1839b19ef..e000c653b87a 100644 --- a/tools/aapt2/ResourceValues.h +++ b/tools/aapt2/ResourceValues.h @@ -65,10 +65,21 @@ class Value { return translatable_; } + void SetFlag(std::optional<FeatureFlagAttribute> val) { + flag_ = val; + } + + std::optional<FeatureFlagAttribute> GetFlag() const { + return flag_; + } + void SetFlagStatus(FlagStatus val) { flag_status_ = val; } + // If the value is behind a flag this returns whether that flag was enabled when the value was + // parsed by comparing it to the flags passed on the command line to aapt2 (taking into account + // negation if necessary). If there was no flag, FlagStatus::NoFlag is returned instead. FlagStatus GetFlagStatus() const { return flag_status_; } @@ -128,6 +139,7 @@ class Value { std::string comment_; bool weak_ = false; bool translatable_ = true; + std::optional<FeatureFlagAttribute> flag_; FlagStatus flag_status_ = FlagStatus::NoFlag; private: diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto index 5c6408940b34..fe9b4a8843ca 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -240,6 +240,9 @@ message Entry { // The staged resource ID of this finalized resource. StagedId staged_id = 7; + + // The set of values defined for this entry which are behind disabled flags + repeated ConfigValue flag_disabled_config_value = 8; } // A Configuration/Value pair. @@ -283,6 +286,8 @@ message Item { // The status of the flag the value is behind if any uint32 flag_status = 8; + bool flag_negated = 9; + string flag_name = 10; } // A CompoundValue is an abstract type. It represents a value that is a made of other values. @@ -297,6 +302,11 @@ message CompoundValue { Plural plural = 5; MacroBody macro = 6; } + + // The status of the flag the value is behind if any + uint32 flag_status = 7; + bool flag_negated = 8; + string flag_name = 9; } // Message holding a boolean, so it can be optionally encoded. diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto index b0ed3da33368..f4735a2f6ce7 100644 --- a/tools/aapt2/ResourcesInternal.proto +++ b/tools/aapt2/ResourcesInternal.proto @@ -49,4 +49,9 @@ message CompiledFile { // Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file). repeated Symbol exported_symbol = 5; + + // The status of the flag the file is behind if any + uint32 flag_status = 6; + bool flag_negated = 7; + string flag_name = 8; } diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 514651e92c27..449d93dd8c0b 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -213,15 +213,28 @@ int Command::Execute(const std::vector<StringPiece>& args, std::ostream* out_err bool match = false; for (Flag& flag : flags_) { - if (arg == flag.name) { + // Allow both "--arg value" and "--arg=value" syntax. + if (arg.starts_with(flag.name) && + (arg.size() == flag.name.size() || (flag.num_args > 0 && arg[flag.name.size()] == '='))) { if (flag.num_args > 0) { - i++; - if (i >= args.size()) { - *out_error << flag.name << " missing argument.\n\n"; - Usage(out_error); - return false; + if (arg.size() == flag.name.size()) { + i++; + if (i >= args.size()) { + *out_error << flag.name << " missing argument.\n\n"; + Usage(out_error); + return 1; + } + arg = args[i]; + } else { + arg.remove_prefix(flag.name.size() + 1); + // Disallow empty arguments after '='. + if (arg.empty()) { + *out_error << flag.name << " has empty argument.\n\n"; + Usage(out_error); + return 1; + } } - flag.action(args[i]); + flag.action(arg); } else { flag.action({}); } diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index 7aa1aa017f7b..20d87e0025c3 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -19,6 +19,7 @@ #include "test/Test.h" using ::testing::Eq; +using namespace std::literals; namespace aapt { @@ -94,4 +95,27 @@ TEST(CommandTest, LongFullyQualifiedPathWindows) { } #endif +TEST(CommandTest, OptionsWithValues) { + TestCommand command; + std::string flag; + command.AddRequiredFlag("--flag", "", &flag); + + ASSERT_EQ(0, command.Execute({"--flag"s, "1"s}, &std::cerr)); + EXPECT_STREQ("1", flag.c_str()); + + ASSERT_EQ(0, command.Execute({"--flag=1"s}, &std::cerr)); + EXPECT_STREQ("1", flag.c_str()); + + ASSERT_EQ(0, command.Execute({"--flag"s, "=2"s}, &std::cerr)); + EXPECT_STREQ("=2", flag.c_str()); + + ASSERT_EQ(0, command.Execute({"--flag"s, "--flag"s}, &std::cerr)); + EXPECT_STREQ("--flag", flag.c_str()); + + EXPECT_NE(0, command.Execute({"--flag"s}, &std::cerr)); + EXPECT_NE(0, command.Execute({"--flag="s}, &std::cerr)); + EXPECT_NE(0, command.Execute({"--flag1=2"s}, &std::cerr)); + EXPECT_NE(0, command.Execute({"--flag1"s, "2"s}, &std::cerr)); +} + } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 2a978a5153ca..a5e18d35a256 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -67,6 +67,7 @@ struct ResourcePathData { std::string resource_dir; std::string name; std::string extension; + std::string flag_name; // Original config str. We keep this because when we parse the config, we may add on // version qualifiers. We want to preserve the original input so the output is easily @@ -81,6 +82,22 @@ static std::optional<ResourcePathData> ExtractResourcePathData(const std::string std::string* out_error, const CompileOptions& options) { std::vector<std::string> parts = util::Split(path, dir_sep); + + std::string flag_name; + // Check for a flag + for (auto iter = parts.begin(); iter != parts.end();) { + if (iter->starts_with("flag(") && iter->ends_with(")")) { + if (!flag_name.empty()) { + if (out_error) *out_error = "resource path cannot contain more than one flag directory"; + return {}; + } + flag_name = iter->substr(5, iter->size() - 6); + iter = parts.erase(iter); + } else { + ++iter; + } + } + if (parts.size() < 2) { if (out_error) *out_error = "bad resource path"; return {}; @@ -131,6 +148,7 @@ static std::optional<ResourcePathData> ExtractResourcePathData(const std::string std::string(dir_str), std::string(name), std::string(extension), + std::move(flag_name), std::string(config_str), config}; } @@ -142,6 +160,9 @@ static std::string BuildIntermediateContainerFilename(const ResourcePathData& da name << "-" << data.config_str; } name << "_" << data.name; + if (!data.flag_name.empty()) { + name << ".(" << data.flag_name << ")"; + } if (!data.extension.empty()) { name << "." << data.extension; } @@ -163,7 +184,6 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, << "failed to open file: " << fin->GetError()); return false; } - // Parse the values file from XML. xml::XmlPullParser xml_parser(fin.get()); @@ -176,6 +196,18 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, // If visibility was forced, we need to use it when creating a new resource and also error if // we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. parser_options.visibility = options.visibility; + parser_options.flag = ParseFlag(path_data.flag_name); + + if (parser_options.flag) { + std::string error; + auto flag_status = GetFlagStatus(parser_options.flag, options.feature_flag_values, &error); + if (flag_status) { + parser_options.flag_status = std::move(flag_status.value()); + } else { + context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error); + return false; + } + } ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config, parser_options); @@ -402,6 +434,18 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options, xmlres->file.config = path_data.config; xmlres->file.source = path_data.source; xmlres->file.type = ResourceFile::Type::kProtoXml; + xmlres->file.flag = ParseFlag(path_data.flag_name); + + if (xmlres->file.flag) { + std::string error; + auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error); + if (flag_status) { + xmlres->file.flag_status = flag_status.value(); + } else { + context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error); + return false; + } + } // Collect IDs that are defined here. XmlIdCollector collector; @@ -491,6 +535,27 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, res_file.source = path_data.source; res_file.type = ResourceFile::Type::kPng; + if (!path_data.flag_name.empty()) { + FeatureFlagAttribute flag; + auto name = path_data.flag_name; + if (name.starts_with('!')) { + flag.negated = true; + flag.name = name.substr(1); + } else { + flag.name = name; + } + res_file.flag = flag; + + std::string error; + auto flag_status = GetFlagStatus(flag, options.feature_flag_values, &error); + if (flag_status) { + res_file.flag_status = flag_status.value(); + } else { + context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error); + return false; + } + } + { auto data = file->OpenAsData(); if (!data) { @@ -540,8 +605,9 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, } // Write the crunched PNG. - if (!android::WritePng(image.get(), nine_patch.get(), &crunched_png_buffer_out, {}, - &source_diag, context->IsVerbose())) { + if (!android::WritePng(image.get(), nine_patch.get(), &crunched_png_buffer_out, + {.compression_level = options.png_compression_level_int}, &source_diag, + context->IsVerbose())) { return false; } @@ -859,6 +925,19 @@ int CompileCommand::Action(const std::vector<std::string>& args) { } } + if (!options_.png_compression_level) { + options_.png_compression_level_int = 9; + } else { + if (options_.png_compression_level->size() != 1 || + options_.png_compression_level->front() < '0' || + options_.png_compression_level->front() > '9') { + context.GetDiagnostics()->Error( + android::DiagMessage() << "PNG compression level should be a number in [0..9] range"); + return 1; + } + options_.png_compression_level_int = options_.png_compression_level->front() - '0'; + } + return Compile(&context, file_collection.get(), archive_writer.get(), options_); } diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index 70c8791524c8..e244546476b0 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -47,6 +47,8 @@ struct CompileOptions { bool verbose = false; std::optional<std::string> product_; FeatureFlagValues feature_flag_values; + std::optional<std::string> png_compression_level; + int png_compression_level_int = 9; }; /** Parses flags and compiles resources to be used in linking. */ @@ -65,6 +67,9 @@ class CompileCommand : public Command { AddOptionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales " "(en-XA and ar-XB)", &options_.pseudolocalize); AddOptionalSwitch("--no-crunch", "Disables PNG processing", &options_.no_png_crunch); + AddOptionalFlag("--png-compression-level", + "Set the zlib compression level for crunched PNG images, [0-9], 9 by default.", + &options_.png_compression_level); AddOptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", &options_.legacy_mode); AddOptionalSwitch("--preserve-visibility-of-styleables", diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp index 6da3176b2bee..d3750a6100d3 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -138,6 +138,22 @@ static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, } } + for (const ResourceConfigValue* config_value_a : entry_a.flag_disabled_values) { + auto config_value_b = entry_b.FindFlagDisabledValue(config_value_a->value->GetFlag().value(), + config_value_a->config); + if (!config_value_b) { + std::stringstream str_stream; + str_stream << "missing disabled value " << pkg_a.name << ":" << type_a.named_type << "/" + << entry_a.name << " config=" << config_value_a->config + << " flag=" << config_value_a->value->GetFlag()->ToString(); + EmitDiffLine(apk_b->GetSource(), str_stream.str()); + diff = true; + } else { + diff |= EmitResourceConfigValueDiff(context, apk_a, pkg_a, type_a, entry_a, config_value_a, + apk_b, pkg_b, type_b, entry_b, config_value_b); + } + } + // Check for any newly added config values. for (const ResourceConfigValue* config_value_b : entry_b.values) { auto config_value_a = entry_a.FindValue(config_value_b->config); @@ -149,6 +165,18 @@ static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, diff = true; } } + for (const ResourceConfigValue* config_value_b : entry_b.flag_disabled_values) { + auto config_value_a = entry_a.FindFlagDisabledValue(config_value_b->value->GetFlag().value(), + config_value_b->config); + if (!config_value_a) { + std::stringstream str_stream; + str_stream << "new disabled config " << pkg_b.name << ":" << type_b.named_type << "/" + << entry_b.name << " config=" << config_value_b->config + << " flag=" << config_value_b->value->GetFlag()->ToString(); + EmitDiffLine(apk_b->GetSource(), str_stream.str()); + diff = true; + } + } return diff; } diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 498e431097ad..232b4024abd2 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -1574,7 +1574,10 @@ class Linker { // If the file path ends with .flata, .jar, .jack, or .zip the file is treated // as ZIP archive and the files within are merged individually. // Otherwise the file is processed on its own. - bool MergePath(const std::string& path, bool override) { + bool MergePath(std::string path, bool override) { + if (path.size() > 2 && util::StartsWith(path, "'") && util::EndsWith(path, "'")) { + path = path.substr(1, path.size() - 2); + } if (util::EndsWith(path, ".flata") || util::EndsWith(path, ".jar") || util::EndsWith(path, ".jack") || util::EndsWith(path, ".zip")) { return MergeArchive(path, override); diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index 7739171b347f..08f8f0d85807 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -34,6 +34,44 @@ using ::android::base::StringPrintf; namespace aapt { +std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text) { + if (!flag_text || flag_text->empty()) { + return {}; + } + FeatureFlagAttribute flag; + if (flag_text->starts_with('!')) { + flag.negated = true; + flag.name = flag_text->substr(1); + } else { + flag.name = flag_text.value(); + } + return flag; +} + +std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag, + const FeatureFlagValues& feature_flag_values, + std::string* out_err) { + if (!flag) { + return FlagStatus::NoFlag; + } + auto flag_it = feature_flag_values.find(flag->name); + if (flag_it == feature_flag_values.end()) { + *out_err = "Resource flag value undefined: " + flag->name; + return {}; + } + const auto& flag_properties = flag_it->second; + if (!flag_properties.read_only) { + *out_err = "Only read only flags may be used with resources: " + flag->name; + return {}; + } + if (!flag_properties.enabled.has_value()) { + *out_err = "Only flags with a value may be used with resources: " + flag->name; + return {}; + } + return (flag_properties.enabled.value() != flag->negated) ? FlagStatus::Enabled + : FlagStatus::Disabled; +} + std::optional<uint16_t> ParseTargetDensityParameter(StringPiece arg, android::IDiagnostics* diag) { ConfigDescription preferred_density_config; if (!ConfigDescription::Parse(arg, &preferred_density_config)) { diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h index 6b8813b34082..d32e532b86a8 100644 --- a/tools/aapt2/cmd/Util.h +++ b/tools/aapt2/cmd/Util.h @@ -49,6 +49,12 @@ struct FeatureFlagProperties { using FeatureFlagValues = std::map<std::string, FeatureFlagProperties, std::less<>>; +std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text); + +std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag, + const FeatureFlagValues& feature_flag_values, + std::string* out_err); + // Parses a configuration density (ex. hdpi, xxhdpi, 234dpi, anydpi, etc). // Returns Nothing and logs a human friendly error message if the string was not legal. std::optional<uint16_t> ParseTargetDensityParameter(android::StringPiece arg, diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index 55f5e5668a16..91ec3485ac3b 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -536,6 +536,32 @@ static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStr config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config, &out_table->string_pool, files, out_error); + + if (config_value->value == nullptr) { + return false; + } + } + + // flag disabled + for (const pb::ConfigValue& pb_config_value : pb_entry.flag_disabled_config_value()) { + const pb::Configuration& pb_config = pb_config_value.config(); + + ConfigDescription config; + if (!DeserializeConfigFromPb(pb_config, &config, out_error)) { + return false; + } + + ResourceConfigValue* config_value = entry->FindOrCreateFlagDisabledValue( + FeatureFlagAttribute{.name = pb_config_value.value().item().flag_name(), + .negated = pb_config_value.value().item().flag_negated()}, + config, pb_config.product()); + if (config_value->value != nullptr) { + *out_error = "duplicate configuration in resource table"; + return false; + } + + config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config, + &out_table->string_pool, files, out_error); if (config_value->value == nullptr) { return false; } @@ -615,6 +641,12 @@ bool DeserializeCompiledFileFromPb(const pb::internal::CompiledFile& pb_file, out_file->source.path = pb_file.source_path(); out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type()); + out_file->flag_status = (FlagStatus)pb_file.flag_status(); + if (!pb_file.flag_name().empty()) { + out_file->flag = + FeatureFlagAttribute{.name = pb_file.flag_name(), .negated = pb_file.flag_negated()}; + } + std::string config_error; if (!DeserializeConfigFromPb(pb_file.config(), &out_file->config, &config_error)) { std::ostringstream error; @@ -748,7 +780,6 @@ std::unique_ptr<Value> DeserializeValueFromPb(const pb::Value& pb_value, if (value == nullptr) { return {}; } - } else if (pb_value.has_compound_value()) { const pb::CompoundValue& pb_compound_value = pb_value.compound_value(); switch (pb_compound_value.value_case()) { @@ -863,6 +894,9 @@ std::unique_ptr<Value> DeserializeValueFromPb(const pb::Value& pb_value, LOG(FATAL) << "unknown compound value: " << (int)pb_compound_value.value_case(); break; } + value->SetFlagStatus((FlagStatus)pb_compound_value.flag_status()); + value->SetFlag(FeatureFlagAttribute{.name = pb_compound_value.flag_name(), + .negated = pb_compound_value.flag_negated()}); } else { LOG(FATAL) << "unknown value: " << (int)pb_value.value_case(); return {}; @@ -1018,6 +1052,10 @@ std::unique_ptr<Item> DeserializeItemFromPb(const pb::Item& pb_item, DeserializeItemFromPbInternal(pb_item, src_pool, config, value_pool, files, out_error); if (item) { item->SetFlagStatus((FlagStatus)pb_item.flag_status()); + if (!pb_item.flag_name().empty()) { + item->SetFlag( + FeatureFlagAttribute{.name = pb_item.flag_name(), .negated = pb_item.flag_negated()}); + } } return item; } diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index 5772b3b0b3e6..fcc77d5a9d6d 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -427,6 +427,14 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(), source_pool.get()); } + + for (const ResourceConfigValue* config_value : entry.flag_disabled_values) { + pb::ConfigValue* pb_config_value = pb_entry->add_flag_disabled_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.get()); + } } } } @@ -721,6 +729,18 @@ void SerializeValueToPb(const Value& value, pb::Value* out_value, android::Strin } if (out_value->has_item()) { out_value->mutable_item()->set_flag_status((uint32_t)value.GetFlagStatus()); + if (value.GetFlag()) { + const auto& flag = value.GetFlag(); + out_value->mutable_item()->set_flag_negated(flag->negated); + out_value->mutable_item()->set_flag_name(flag->name); + } + } else if (out_value->has_compound_value()) { + out_value->mutable_compound_value()->set_flag_status((uint32_t)value.GetFlagStatus()); + if (value.GetFlag()) { + const auto& flag = value.GetFlag(); + out_value->mutable_compound_value()->set_flag_negated(flag->negated); + out_value->mutable_compound_value()->set_flag_name(flag->name); + } } } @@ -730,6 +750,11 @@ void SerializeItemToPb(const Item& item, pb::Item* out_item) { item.Accept(&serializer); out_item->MergeFrom(value.item()); out_item->set_flag_status((uint32_t)item.GetFlagStatus()); + if (item.GetFlag()) { + const auto& flag = item.GetFlag(); + out_item->set_flag_negated(flag->negated); + out_item->set_flag_name(flag->name); + } } void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledFile* out_file) { @@ -737,6 +762,11 @@ void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledF out_file->set_source_path(file.source.path); out_file->set_type(SerializeFileReferenceTypeToPb(file.type)); SerializeConfig(file.config, out_file->mutable_config()); + out_file->set_flag_status((uint32_t)file.flag_status); + if (file.flag) { + out_file->set_flag_negated(file.flag->negated); + out_file->set_flag_name(file.flag->name); + } for (const SourcedResourceName& exported : file.exported_symbols) { pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol(); diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp index c456e5c296d2..1b0f99753ce6 100644 --- a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp @@ -30,14 +30,32 @@ genrule { "res/values/bools2.xml", "res/values/ints.xml", "res/values/strings.xml", + "res/values/styles.xml", "res/layout/layout1.xml", + "res/layout/layout3.xml", + "res/flag(test.package.falseFlag)/values/bools.xml", + "res/flag(test.package.falseFlag)/layout/layout2.xml", + "res/flag(test.package.falseFlag)/drawable/removedpng.png", + "res/flag(test.package.trueFlag)/layout/layout3.xml", + "res/values/flag(test.package.trueFlag)/bools.xml", + "res/values/flag(!test.package.trueFlag)/bools.xml", + "res/values/flag(!test.package.falseFlag)/bools.xml", ], out: [ + "drawable_removedpng.(test.package.falseFlag).png.flat", "values_bools.arsc.flat", + "values_bools.(test.package.falseFlag).arsc.flat", + "values_bools.(test.package.trueFlag).arsc.flat", + "values_bools.(!test.package.falseFlag).arsc.flat", + "values_bools.(!test.package.trueFlag).arsc.flat", "values_bools2.arsc.flat", "values_ints.arsc.flat", "values_strings.arsc.flat", + "values_styles.arsc.flat", "layout_layout1.xml.flat", + "layout_layout2.(test.package.falseFlag).xml.flat", + "layout_layout3.xml.flat", + "layout_layout3.(test.package.trueFlag).xml.flat", ], cmd: "$(location aapt2) compile $(in) -o $(genDir) " + "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true", diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png Binary files differnew file mode 100644 index 000000000000..8a9e6984be96 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml new file mode 100644 index 000000000000..dec5de72925a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > +</LinearLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/values/bools.xml new file mode 100644 index 000000000000..c46c4d4d8546 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/values/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool7">false</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/layout/layout3.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/layout/layout3.xml new file mode 100644 index 000000000000..5aeee0ee1e28 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/layout/layout3.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + <TextView android:id="@+id/text1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="foobar" /> +</LinearLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml new file mode 100644 index 000000000000..dec5de72925a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > +</LinearLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml index 1ed0c8a5f1e6..35975ed1274a 100644 --- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml @@ -9,4 +9,15 @@ <bool name="bool3">false</bool> <bool name="bool4" android:featureFlag="test.package.falseFlag">true</bool> + + <bool name="bool5">false</bool> + <bool name="bool5" android:featureFlag="!test.package.falseFlag">true</bool> + + <bool name="bool6">true</bool> + <bool name="bool6" android:featureFlag="!test.package.trueFlag">false</bool> + + <bool name="bool7">true</bool> + <bool name="bool8">false</bool> + <bool name="bool9">true</bool> + <bool name="bool10">false</bool> </resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/bools.xml new file mode 100644 index 000000000000..a63749c6ed7e --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool10">true</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/bools.xml new file mode 100644 index 000000000000..bb5526e69f97 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool9">false</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/bools.xml new file mode 100644 index 000000000000..eba780e88c9a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool8">true</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/styles.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/styles.xml new file mode 100644 index 000000000000..604129c26fef --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/styles.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <style name="style1"> + <item name="android:windowIsTranslucent">true</item> + </style> + <style name="style1" android:featureFlag="test.package.falseFlag"> + <item name="android:windowIsTranslucent">false</item> + </style> + + <style name="style2"> + <item name="android:windowIsTranslucent">false</item> + </style> + <style name="style2" android:featureFlag="test.package.trueFlag"> + <item name="android:windowIsTranslucent">true</item> + </style> + + <style name="style3"> + <item name="android:windowIsTranslucent" android:featureFlag="!test.package.trueFlag">false</item> + <item name="android:windowIsTranslucent" android:featureFlag="test.package.trueFlag">true</item> + </style> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp index 3db37c2fa6f8..629300838bbe 100644 --- a/tools/aapt2/link/FlaggedResources_test.cpp +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -17,6 +17,7 @@ #include "LoadedApk.h" #include "cmd/Dump.h" #include "io/StringStream.h" +#include "test/Common.h" #include "test/Test.h" #include "text/Printer.h" @@ -75,6 +76,10 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTable) { std::string output; DumpResourceTableToString(loaded_apk.get(), &output); + ASSERT_EQ(output.find("bool4"), std::string::npos); + ASSERT_EQ(output.find("str1"), std::string::npos); + ASSERT_EQ(output.find("layout2"), std::string::npos); + ASSERT_EQ(output.find("removedpng"), std::string::npos); } TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) { @@ -86,6 +91,8 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) { ASSERT_EQ(output.find("bool4"), std::string::npos); ASSERT_EQ(output.find("str1"), std::string::npos); + ASSERT_EQ(output.find("layout2"), std::string::npos); + ASSERT_EQ(output.find("removedpng"), std::string::npos); } TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) { @@ -98,4 +105,47 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) { ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos); } +TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlag) { + test::TestDiagnosticsImpl diag; + const std::string compiled_files_dir = GetTestPath("compiled"); + ASSERT_FALSE(CompileFile( + GetTestPath("res/values/values.xml"), + R"(<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool> + <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool> + </resources>)", + compiled_files_dir, &diag, + {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"})); + ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool/bool1'")); +} + +TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlagDifferentFiles) { + test::TestDiagnosticsImpl diag; + const std::string compiled_files_dir = GetTestPath("compiled"); + ASSERT_TRUE(CompileFile( + GetTestPath("res/values/values1.xml"), + R"(<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool> + </resources>)", + compiled_files_dir, &diag, + {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"})); + ASSERT_TRUE(CompileFile( + GetTestPath("res/values/values2.xml"), + R"(<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool> + </resources>)", + compiled_files_dir, &diag, + {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"})); + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", + GetDefaultManifest(), + "-o", + out_apk, + }; + + ASSERT_FALSE(Link(link_args, compiled_files_dir, &diag)); + ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool1'")); +} + } // namespace aapt diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index 37a039e9528f..1d4adc4a57d8 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -207,14 +207,13 @@ static ResourceTable::CollisionResult MergeConfigValue( Value* dst_value = dst_config_value->value.get(); Value* src_value = src_config_value->value.get(); - CollisionResult collision_result; - if (overlay) { - collision_result = - ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool); - } else { - collision_result = - ResourceTable::ResolveFlagCollision(dst_value->GetFlagStatus(), src_value->GetFlagStatus()); - if (collision_result == CollisionResult::kConflict) { + CollisionResult collision_result = + ResourceTable::ResolveFlagCollision(dst_value->GetFlagStatus(), src_value->GetFlagStatus()); + if (collision_result == CollisionResult::kConflict) { + if (overlay) { + collision_result = + ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool); + } else { collision_result = ResourceTable::ResolveValueCollision(dst_value, src_value); } } @@ -321,6 +320,30 @@ bool TableMerger::DoMerge(const android::Source& src, ResourceTablePackage* src_ } } } + + // disabled values + for (auto& src_config_value : src_entry->flag_disabled_values) { + auto dst_config_value = dst_entry->FindOrCreateFlagDisabledValue( + src_config_value->value->GetFlag().value(), src_config_value->config, + src_config_value->product); + if (!dst_config_value->value) { + // Resource does not exist, add it now. + // Must clone the value since it might be in the values vector as well + CloningValueTransformer cloner(&main_table_->string_pool); + dst_config_value->value = src_config_value->value->Transform(cloner); + } else { + error = true; + context_->GetDiagnostics()->Error( + android::DiagMessage(src_config_value->value->GetSource()) + << "duplicate value for resource '" << src_entry->name << "' " << "with config '" + << src_config_value->config << "' and flag '" + << (src_config_value->value->GetFlag()->negated ? "!" : "") + << src_config_value->value->GetFlag()->name << "'"); + context_->GetDiagnostics()->Note( + android::DiagMessage(dst_config_value->value->GetSource()) + << "resource previously defined here"); + } + } } } return !error; @@ -353,6 +376,8 @@ bool TableMerger::MergeFile(const ResourceFile& file_desc, bool overlay, io::IFi file_ref->SetSource(file_desc.source); file_ref->type = file_desc.type; file_ref->file = file; + file_ref->SetFlagStatus(file_desc.flag_status); + file_ref->SetFlag(file_desc.flag); ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package); pkg->FindOrCreateType(file_desc.name.type) diff --git a/tools/aapt2/test/Common.cpp b/tools/aapt2/test/Common.cpp index cdf245341844..c7dd4c90e67f 100644 --- a/tools/aapt2/test/Common.cpp +++ b/tools/aapt2/test/Common.cpp @@ -21,23 +21,6 @@ using android::ConfigDescription; namespace aapt { namespace test { -struct TestDiagnosticsImpl : public android::IDiagnostics { - void Log(Level level, android::DiagMessageActual& actual_msg) override { - switch (level) { - case Level::Note: - return; - - case Level::Warn: - std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl; - break; - - case Level::Error: - std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl; - break; - } - } -}; - android::IDiagnostics* GetDiagnostics() { static TestDiagnosticsImpl diag; return &diag; diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h index 04379804d8ee..b06c4329488e 100644 --- a/tools/aapt2/test/Common.h +++ b/tools/aapt2/test/Common.h @@ -37,6 +37,32 @@ namespace aapt { namespace test { +struct TestDiagnosticsImpl : public android::IDiagnostics { + void Log(Level level, android::DiagMessageActual& actual_msg) override { + switch (level) { + case Level::Note: + return; + + case Level::Warn: + std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl; + log << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl; + break; + + case Level::Error: + std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl; + log << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl; + break; + } + } + + std::string GetLog() { + return log.str(); + } + + private: + std::ostringstream log; +}; + android::IDiagnostics* GetDiagnostics(); inline ResourceName ParseNameOrDie(android::StringPiece str) { diff --git a/tools/aapt2/test/Fixture.cpp b/tools/aapt2/test/Fixture.cpp index b91abe572306..570bcf16c92c 100644 --- a/tools/aapt2/test/Fixture.cpp +++ b/tools/aapt2/test/Fixture.cpp @@ -91,10 +91,13 @@ void TestDirectoryFixture::WriteFile(const std::string& path, const std::string& } bool CommandTestFixture::CompileFile(const std::string& path, const std::string& contents, - android::StringPiece out_dir, android::IDiagnostics* diag) { + android::StringPiece out_dir, android::IDiagnostics* diag, + const std::vector<android::StringPiece>& additional_args) { WriteFile(path, contents); CHECK(file::mkdirs(out_dir.data())); - return CompileCommand(diag).Execute({path, "-o", out_dir, "-v"}, &std::cerr) == 0; + std::vector<android::StringPiece> args = {path, "-o", out_dir, "-v"}; + args.insert(args.end(), additional_args.begin(), additional_args.end()); + return CompileCommand(diag).Execute(args, &std::cerr) == 0; } bool CommandTestFixture::Link(const std::vector<std::string>& args, android::IDiagnostics* diag) { diff --git a/tools/aapt2/test/Fixture.h b/tools/aapt2/test/Fixture.h index 14298d1678f0..178d01156f32 100644 --- a/tools/aapt2/test/Fixture.h +++ b/tools/aapt2/test/Fixture.h @@ -73,7 +73,8 @@ class CommandTestFixture : public TestDirectoryFixture { // Wries the contents of the file to the specified path. The file is compiled and the flattened // file is written to the out directory. bool CompileFile(const std::string& path, const std::string& contents, - android::StringPiece flat_out_dir, android::IDiagnostics* diag); + android::StringPiece flat_out_dir, android::IDiagnostics* diag, + const std::vector<android::StringPiece>& additional_args = {}); // Executes the link command with the specified arguments. bool Link(const std::vector<std::string>& args, android::IDiagnostics* diag); diff --git a/tools/aapt2/util/Util.h b/tools/aapt2/util/Util.h index 40ff5b633d97..03da460affb5 100644 --- a/tools/aapt2/util/Util.h +++ b/tools/aapt2/util/Util.h @@ -111,8 +111,8 @@ const char* GetToolName(); // Retrieves the build fingerprint of aapt2. std::string GetToolFingerprint(); -template <typename T> -typename std::enable_if<std::is_arithmetic<T>::value, int>::type compare(const T& a, const T& b) { +template <std::integral T> +int compare(T a, T b) { if (a < b) { return -1; } else if (a > b) { @@ -123,10 +123,7 @@ typename std::enable_if<std::is_arithmetic<T>::value, int>::type compare(const T // Makes a std::unique_ptr<> with the template parameter inferred by the compiler. // This will be present in C++14 and can be removed then. -template <typename T, class... Args> -std::unique_ptr<T> make_unique(Args&&... args) { - return std::unique_ptr<T>(new T{std::forward<Args>(args)...}); -} +using std::make_unique; // Writes a set of items to the std::ostream, joining the times with the provided separator. template <typename Container> diff --git a/tools/processors/property_cache/Android.bp b/tools/processors/property_cache/Android.bp new file mode 100644 index 000000000000..81fab7a4c862 --- /dev/null +++ b/tools/processors/property_cache/Android.bp @@ -0,0 +1,57 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_framework_android_multiuser", +} + +java_library_host { + name: "libcached-property-annotation-processor", + srcs: [ + ":framework-annotations", + "src/**/*.java", + ], + static_libs: [ + "codegen-version-info", + "android.multiuser.flags-aconfig-java-host", + "guava", + ], + use_tools_jar: true, +} + +java_plugin { + name: "cached-property-annotation-processor", + processor_class: "android.processor.property_cache.CachedPropertyProcessor", + static_libs: ["libcached-property-annotation-processor"], +} + +java_aconfig_library { + name: "android.multiuser.flags-aconfig-java-host", + aconfig_declarations: "android.multiuser.flags-aconfig", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + +java_test_host { + name: "cached-property-annotation-processor-test", + srcs: ["test/java/**/*.java"], + java_resources: [":CachedPropertyAnnotationJavaTestSource"], + static_libs: [ + "compile-testing-prebuilt", + "truth", + "junit", + "guava", + "libcached-property-annotation-processor", + ], + test_suites: ["general-tests"], +} + +filegroup { + name: "CachedPropertyAnnotationJavaTestSource", + srcs: ["test/resources/*.java"], + path: "test/resources/", + visibility: ["//visibility:private"], +} diff --git a/tools/processors/property_cache/OWNERS b/tools/processors/property_cache/OWNERS new file mode 100644 index 000000000000..78650168807e --- /dev/null +++ b/tools/processors/property_cache/OWNERS @@ -0,0 +1,3 @@ +include /ACTIVITY_MANAGER_OWNERS +include /BROADCASTS_OWNERS +include /MULTIUSER_OWNERS
\ No newline at end of file diff --git a/tools/processors/property_cache/TEST_MAPPING b/tools/processors/property_cache/TEST_MAPPING new file mode 100644 index 000000000000..7177abc8c52d --- /dev/null +++ b/tools/processors/property_cache/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "cached-property-annotation-processor-test" + } + ] +} diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java new file mode 100644 index 000000000000..c665c840f376 --- /dev/null +++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache; + +import com.android.internal.annotations.CachedProperty; +import com.android.internal.annotations.CachedPropertyDefaults; + +import com.google.common.base.CaseFormat; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +public class CacheConfig { + private final CacheModifiers mModifiers; + private final int mMaxSize; + private final String mModuleName; + private final String mApiName; + private final String mClassName; + private final String mQualifiedName; + private String mPropertyName; + private String mMethodName; + private int mNumberOfParams = 0; + private String mInputType = Constants.JAVA_LANG_VOID; + private String mResultType; + + public CacheConfig(TypeElement classElement, ExecutableElement method) { + CachedPropertyDefaults classAnnotation = classElement.getAnnotation( + CachedPropertyDefaults.class); + CachedProperty methodAnnotation = method.getAnnotation(CachedProperty.class); + + mModuleName = methodAnnotation.module().isEmpty() ? classAnnotation.module() + : methodAnnotation.module(); + mClassName = classElement.getSimpleName().toString(); + mQualifiedName = classElement.getQualifiedName().toString(); + mModifiers = new CacheModifiers(methodAnnotation.modsFlagOnOrNone()); + mMethodName = method.getSimpleName().toString(); + mPropertyName = getPropertyName(mMethodName); + mApiName = methodAnnotation.api().isEmpty() ? getUniqueApiName(mClassName, mPropertyName) + : methodAnnotation.api(); + mMaxSize = methodAnnotation.max() == -1 ? classAnnotation.max() : methodAnnotation.max(); + mNumberOfParams = method.getParameters().size(); + if (mNumberOfParams > 0) { + mInputType = primitiveTypeToObjectEquivalent( + method.getParameters().get(0).asType().toString()); + } + mResultType = primitiveTypeToObjectEquivalent(method.getReturnType().toString()); + } + + public CacheModifiers getModifiers() { + return mModifiers; + } + + public int getMaxSize() { + return mMaxSize; + } + + public String getApiName() { + return mApiName; + } + + public String getClassName() { + return mClassName; + } + + public String getQualifiedName() { + return mQualifiedName; + } + + public String getModuleName() { + return mModuleName; + } + + public String getMethodName() { + return mMethodName; + } + + public String getPropertyName() { + return mPropertyName; + } + + public String getPropertyVariable() { + return (mModifiers.isStatic() ? "s" : "m") + mPropertyName; + } + + private String getPropertyName(String methodName) { + if (methodName.startsWith("get")) { + return methodName.substring(3); + } else if (methodName.startsWith("is")) { + return methodName.substring(2); + } else { + return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, methodName); + } + } + + public int getNumberOfParams() { + return mNumberOfParams; + } + + public String getInputType() { + return mInputType; + } + + public String getResultType() { + return mResultType; + } + + /** + * This method returns the unique api name for a given class and property name. + * Property name is retrieved from the method name. + * Both names are combined and converted to lower snake case. + * + * @param className The name of the class that contains the property. + * @param propertyName The name of the property. + * @return The registration name for the property. + */ + private String getUniqueApiName(String className, String propertyName) { + return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, className + propertyName); + } + + private String primitiveTypeToObjectEquivalent(String simpleType) { + // checking against primitive types + return Constants.PRIMITIVE_TYPE_MAP.getOrDefault(simpleType, simpleType); + } +} diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java new file mode 100644 index 000000000000..fda9b2c40e27 --- /dev/null +++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache; + +import com.android.internal.annotations.CacheModifier; + +import java.util.Arrays; +import java.util.List; + +public class CacheModifiers { + private final boolean mIsStatic; + private static final String STATIC_MODIFIER_STRING = "static "; + + CacheModifiers(CacheModifier[] modifierArray) { + final List<CacheModifier> modifiers = Arrays.asList(modifierArray); + mIsStatic = modifiers.contains(CacheModifier.STATIC); + } + + public boolean isStatic() { + return mIsStatic; + } + + public String getStaticModifier() { + return mIsStatic ? STATIC_MODIFIER_STRING : Constants.EMPTY_STRING; + } +} diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java new file mode 100644 index 000000000000..c438163948fc --- /dev/null +++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache; + +import com.android.internal.annotations.CachedProperty; +import com.android.internal.annotations.CachedPropertyDefaults; + +import com.google.common.collect.ImmutableSet; + +import java.io.IOException; +import java.io.Writer; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.ElementFilter; +import javax.tools.JavaFileObject; + +public class CachedPropertyProcessor extends AbstractProcessor { + + IpcDataCacheComposer mIpcDataCacheComposer = + new IpcDataCacheComposer(); + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public Set<String> getSupportedAnnotationTypes() { + return new HashSet<String>( + ImmutableSet.of(CachedPropertyDefaults.class.getCanonicalName())); + } + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + for (Element element : roundEnv.getElementsAnnotatedWith(CachedPropertyDefaults.class)) { + try { + generateCachedClass((TypeElement) element, processingEnv.getFiler()); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + return false; + } + + private void generateCachedClass(TypeElement classElement, Filer filer) throws IOException { + String packageName = + processingEnv + .getElementUtils() + .getPackageOf(classElement) + .getQualifiedName() + .toString(); + String className = classElement.getSimpleName().toString() + "Cache"; + JavaFileObject jfo = filer.createSourceFile(packageName + "." + className); + Writer writer = jfo.openWriter(); + writer.write("package " + packageName + ";\n\n"); + writer.write("import android.os.IpcDataCache;\n"); + writer.write("\n /** \n * This class is auto-generated \n * @hide \n **/"); + writer.write("\npublic class " + className + " {\n"); + + List<ExecutableElement> methods = + ElementFilter.methodsIn(classElement.getEnclosedElements()); + String initCache = String.format(Constants.METHOD_COMMENT, + " - initialise all caches for class " + className) + + "\npublic static void initCache() {"; + for (ExecutableElement method : methods) { + if (method.getAnnotation(CachedProperty.class) != null) { + mIpcDataCacheComposer.generatePropertyCache(writer, classElement, method); + initCache += "\n " + mIpcDataCacheComposer.generateInvalidatePropertyCall(); + } + } + initCache += "\n}"; + writer.write(initCache); + writer.write("\n}"); + writer.write("\n"); + writer.close(); + } +} diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java b/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java new file mode 100644 index 000000000000..03961bcaaba0 --- /dev/null +++ b/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache; + +import com.google.common.collect.ImmutableMap; + +public final class Constants { + public static final String EMPTY_STRING = ""; + public static final String JAVA_LANG_VOID = "java.lang.Void"; + public static final ImmutableMap<String, String> PRIMITIVE_TYPE_MAP = + ImmutableMap.of( + "int", "java.lang.Integer", + "boolean", "java.lang.Boolean", + "long", "java.lang.Long", + "float", "java.lang.Float", + "double", "java.lang.Double", + "byte", "java.lang.Byte", + "short", "java.lang.Short", + "char", "java.lang.Character"); + + public static final String METHOD_COMMENT = "\n /**" + + "\n * This method is auto-generated%s" + + "\n * " + + "\n * @hide" + + "\n */"; +} diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java b/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java new file mode 100644 index 000000000000..8526a04e9910 --- /dev/null +++ b/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache; + +import java.io.IOException; +import java.io.Writer; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +public class IpcDataCacheComposer { + + private static final String PROPERTY_DEFINITION_LINE = "private %s%s %s;\n"; + private static final String METHOD_NAME_LINE = "\npublic %s%s %s(%s%s%s\n) {\n"; + private static final String RETURN_IF_NOT_NULL_LINE = + "if (%s != null) {\n return %s.%s;\n }"; + + private CacheConfig mCacheConfig; + + /** + * Generates code for property cache. + * + * @param writer writer to write code to. + * @param classElement class element to generate code for. + * @param method method element to generate code for. + * @throws IOException if writer throws IOException. + */ + public void generatePropertyCache(Writer writer, TypeElement classElement, + ExecutableElement method) throws IOException { + + mCacheConfig = new CacheConfig(classElement, method); + + ParamComposer inputParam = new ParamComposer(null, null); + ParamComposer binderParam = new ParamComposer( + String.format("IpcDataCache.RemoteCall<%s, %s>", mCacheConfig.getInputType(), + mCacheConfig.getResultType()), "binderCall"); + + ParamComposer bypassParam = new ParamComposer(null, null); // empty if method have no params + String queryCall = "query(null)"; + if (mCacheConfig.getNumberOfParams() > 0) { + bypassParam = new ParamComposer( + String.format("IpcDataCache.BypassCall<%s> ", mCacheConfig.getInputType()), + "bypassPredicate"); + inputParam = new ParamComposer(mCacheConfig.getInputType(), "query"); + queryCall = "query(query)"; + } + String propertyClass = + "IpcDataCache<" + mCacheConfig.getInputType() + ", " + mCacheConfig.getResultType() + + ">"; + String invalidateName = "invalidate" + mCacheConfig.getPropertyName(); + String lockObject = mCacheConfig.getPropertyVariable() + "Lock"; + writer.write("private " + mCacheConfig.getModifiers().getStaticModifier() + "final Object " + + lockObject + " = new Object();\n"); + writer.write(String.format(PROPERTY_DEFINITION_LINE, + mCacheConfig.getModifiers().getStaticModifier(), propertyClass, + mCacheConfig.getPropertyVariable())); + + writer.write(propertyInvalidatedCacheMethod(binderParam, bypassParam, inputParam, queryCall, + lockObject)); + + // If binder param is not empty then generate getter without binder param to be called + if (!bypassParam.getParam().isEmpty()) { + writer.write(propertyInvalidatedCacheMethod(binderParam, new ParamComposer(null, null), + inputParam, queryCall, lockObject)); + } + writer.write(String.format(Constants.METHOD_COMMENT, + "- invalidate cache for {@link " + mCacheConfig.getQualifiedName() + "#" + + mCacheConfig.getMethodName() + "}")); + writer.write("\n public static final void " + invalidateName + "() {"); + writer.write( + "\n IpcDataCache.invalidateCache(\"" + mCacheConfig.getModuleName() + "\", \"" + + mCacheConfig.getApiName() + "\");"); + writer.write("\n }"); + writer.write("\n"); + writer.write("\n"); + } + + /** + * Generates code to call cache invalidation. + * + * @return code string calling cache invalidation. + */ + public String generateInvalidatePropertyCall() { + String invalidateName = "invalidate" + mCacheConfig.getPropertyName(); + return mCacheConfig.getClassName() + "Cache." + invalidateName + "();"; + } + + /** + * Generates code for getter that returns cached value or calls binder and caches result. + * + * @param binderParam parameter for binder call. + * @param bypassParam parameter for bypass predicate. + * @param inputParam parameter for input value. + * @param queryCall cache query call syntax. + * @param lockObject object to synchronize on. + * @return String with code for method. + */ + private String propertyInvalidatedCacheMethod(ParamComposer binderParam, + ParamComposer bypassParam, ParamComposer inputParam, String queryCall, + String lockObject) { + String result = "\n"; + CacheModifiers modifiers = mCacheConfig.getModifiers(); + String paramsComments = binderParam.getParamComment( + "lambda for remote call" + " {@link " + mCacheConfig.getQualifiedName() + "#" + + mCacheConfig.getMethodName() + " }") + bypassParam.getParamComment( + "lambda to bypass remote call") + inputParam.getParamComment( + "parameter to call remote lambda"); + result += String.format(Constants.METHOD_COMMENT, paramsComments); + result += String.format(METHOD_NAME_LINE, modifiers.getStaticModifier(), + mCacheConfig.getResultType(), mCacheConfig.getMethodName(), + binderParam.getParam(), bypassParam.getNextParam(), + inputParam.getNextParam()); + result += String.format(RETURN_IF_NOT_NULL_LINE, mCacheConfig.getPropertyVariable(), + mCacheConfig.getPropertyVariable(), queryCall); + result += "\n synchronized (" + lockObject + " ) {"; + result += "\n if (" + mCacheConfig.getPropertyVariable() + " == null) {"; + result += "\n " + mCacheConfig.getPropertyVariable() + " = new IpcDataCache" + "(" + + generateCreateIpcConfig() + ", " + binderParam.getName() + + bypassParam.getNextName() + ");\n"; + result += "\n }"; + result += "\n }"; + result += "\n return " + mCacheConfig.getPropertyVariable() + "." + queryCall + ";"; + result += "\n }"; + result += "\n"; + return result; + } + + /** + * Generates code for new IpcDataCache.Config object for given configuration. + * + * @return String with code for new IpcDataCache.Config object. + */ + public String generateCreateIpcConfig() { + return "new IpcDataCache.Config(" + mCacheConfig.getMaxSize() + ", " + "\"" + + mCacheConfig.getModuleName() + "\"" + ", " + "\"" + mCacheConfig.getApiName() + + "\"" + ", " + "\"" + mCacheConfig.getPropertyName() + "\"" + ")"; + } +} diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java b/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java new file mode 100644 index 000000000000..307443aea730 --- /dev/null +++ b/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache; + +public class ParamComposer { + private String mType; + private String mName; + + /** Creates ParamComposer with given type and name. + * + * @param type type of parameter. + * @param name name of parameter. + */ + public ParamComposer(String type, String name) { + mType = type; + mName = name; + } + + /** Returns name of parameter. + * + * @return name of parameter. + */ + public String getName() { + if (mName != null) { + return mName; + } + return Constants.EMPTY_STRING; + } + + /** Returns name of parameter for next parameter followed by comma. + * + * @return name of parameter for next parameter if exists, empty string otherwise. + */ + public String getNextName() { + if (!getName().isEmpty()) { + return ", " + getName(); + } + return Constants.EMPTY_STRING; + } + + /** + * Returns type of parameter. + * + * @return type of parameter. + */ + public String getType() { + if (mType != null) { + return mType; + } + return Constants.EMPTY_STRING; + } + + /** + * Returns type and name of parameter. + * + * @return type and name of parameter if exists, empty string otherwise. + */ + public String getParam() { + if (!getType().isEmpty() && !getName().isEmpty()) { + return getType() + " " + getName(); + } + return Constants.EMPTY_STRING; + } + + /** + * Returns type and name of parameter for next parameter followed by comma. + * + * @return type and name of parameter for next parameter if exists, empty string otherwise. + */ + public String getNextParam() { + if (!getType().isEmpty() && !getName().isEmpty()) { + return ", " + getParam(); + } + return Constants.EMPTY_STRING; + } + + /** + * Returns comment for parameter. + * + * @param description of parameter. + * @return comment for parameter if exists, empty string otherwise. + */ + public String getParamComment(String description) { + if (!getType().isEmpty() && !getName().isEmpty()) { + return "\n * @param " + getName() + " - " + description; + } + return Constants.EMPTY_STRING; + } +} diff --git a/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java b/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java new file mode 100644 index 000000000000..1e23c78d4816 --- /dev/null +++ b/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache.test; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.testing.compile.CompilationSubject.assertThat; + +import android.processor.property_cache.CachedPropertyProcessor; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.Compiler; +import com.google.testing.compile.JavaFileObjects; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +/** Tests the {@link CachedPropertyProcessor}. */ +@RunWith(JUnit4.class) +public class CachedPropertyProcessorTest { + private final Compiler mCompiler = + Compiler.javac().withProcessors(new CachedPropertyProcessor()); + + @Test + public void testDefaultValues() { + JavaFileObject expectedJava = JavaFileObjects.forResource("DefaultCache.java"); + + Compilation compilation = mCompiler.compile(JavaFileObjects.forResource("Default.java")); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedFile(StandardLocation.SOURCE_OUTPUT, + "android/processor/property_cache/test/DefaultCache.java") + .hasSourceEquivalentTo(expectedJava); + } + + @Test + public void testCustomValues() { + JavaFileObject expectedJava = JavaFileObjects.forResource("CustomCache.java"); + + Compilation compilation = mCompiler.compile(JavaFileObjects.forResource("Custom.java")); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedFile(StandardLocation.SOURCE_OUTPUT, + "android/processor/property_cache/test/CustomCache.java") + .hasSourceEquivalentTo(expectedJava); + } +} diff --git a/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java b/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java new file mode 100644 index 000000000000..e5ef48c14436 --- /dev/null +++ b/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +// Mocked class for generation compilation tests purposes only. +public class IpcDataCache<Input, Output> { + public static class Config { + public Config(int max, String module, String api, String name) { + } + } + + /** Shadow method for generated code compilation tests purposes only. + * + * @param query - shadow parameter from IpcDataCache in Frameworks. + * @return null + */ + public Output query(Input query) { + return null; + } + + /** Shadow method for generated code compilation tests purposes only. + * + * @param key - shadow parameter from IpcDataCache in Frameworks; + */ + public static void invalidateCache(String key) { + } + + /** Shadow method for generated code compilation tests purposes only. + * + * @param query - shadow parameter from IpcDataCache in Frameworks; + * @return null + */ + public Output recompute(Input query) { + return null; + } + + /** Shadow method for generated code compilation tests purposes only. + * + * @param query - parameter equivalent to IpcDataCache in android framework. + * @param query - shadow parameter from IpcDataCache in Frameworks; + * @return false + */ + public boolean bypass(Input query) { + return false; + } + + /** Shadow method for generated code compilation tests purposes only. + * + * @param module - parameter equivalent to IpcDataCache in android framework. + * @param key - parameter equivalent to IpcDataCache in android framework. + * @return module + key sttring + */ + public static String createPropertyName(String module, String key) { + return module + key; + } + + public abstract static class QueryHandler<Input, Output> { + /** Shadow method for generated code compilation tests purposes only. + * + * @param query - parameter equivalent to IpcDataCache.QueryHandler in android framework. + * @return expected value + */ + public abstract Output apply(Input query); + /** Shadow method for generated code compilation tests purposes only. + * + * @param query - parameter equivalent to IpcDataCache.QueryHandler in android framework. + */ + public boolean shouldBypassCache(Input query) { + return false; + } + } + + public interface RemoteCall<Input, Output> { + /** Shadow method for generated code compilation tests purposes only. + * + * @param query - parameter equivalent to IpcDataCache.RemoteCall in android framework. + */ + Output apply(Input query); + } + + public interface BypassCall<Input> { + /** Shadow method for generated code compilation tests purposes only. + * + * @param query - parameter equivalent to IpcDataCache.BypassCall in android framework. + */ + boolean apply(Input query); + } + + public IpcDataCache( + int maxEntries, + String module, + String api, + String cacheName, + QueryHandler<Input, Output> computer) { + } + + public IpcDataCache(Config config, QueryHandler<Input, Output> computer) { + } + + public IpcDataCache(Config config, RemoteCall<Input, Output> computer) { + } + + public IpcDataCache(Config config, RemoteCall<Input, Output> computer, + BypassCall<Input> bypassCall) { + } + + /** Shadow method for generated code compilation tests purposes only.*/ + public void invalidateCache() { + } + + + /** Shadow method for generated code compilation tests purposes only. + * + * @param module - shadow parameter from IpcDataCache in Frameworks. + * @param api - shadow parameter from IpcDataCache in Frameworks. + */ + public static void invalidateCache(String module, String api) { + } + +} diff --git a/tools/processors/property_cache/test/resources/Custom.java b/tools/processors/property_cache/test/resources/Custom.java new file mode 100644 index 000000000000..05024da67e6c --- /dev/null +++ b/tools/processors/property_cache/test/resources/Custom.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache.test; + +import com.android.internal.annotations.CacheModifier; +import com.android.internal.annotations.CachedProperty; +import com.android.internal.annotations.CachedPropertyDefaults; + +import java.util.Date; + +@CachedPropertyDefaults(max = 4, module = "bluetooth") +public class Custom { + BirthdayManagerService mService = new BirthdayManagerService(); + Object mCache = new CustomCache(); + + public Custom() { + CustomCache.initCache(); + } + + /** + * Testing custom class values to generate static IpcDataCache + * + * @param userId - user Id + * @return birthday date of given user Id + */ + @CachedProperty() + public Date getBirthday(int userId) { + return CustomCache.getBirthday(mService::getBirthday, userId); + } + + /** + * Testing custom class values to generate static IpcDataCache + * + * @param userId - user Id + * @return number of days till birthday of given user Id + */ + @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC}) + public int getDaysTillBirthday(int userId) { + return CustomCache.getDaysTillBirthday(mService::getDaysTillBirthday, userId); + } + + /** + * Testing custom class values to generate non-static IpcDataCache + * + * @param userId - user Id + * @return number of days since birthday of given user Id + */ + @CachedProperty(modsFlagOnOrNone = {}) + public int getDaysSinceBirthday(int userId) { + return ((CustomCache) mCache).getDaysSinceBirthday(mService::getDaysSinceBirthday, userId); + } + + /** + * Testing custom class values to generate static IpcDataCache with max capasity of 1 + * + * @return number of days till birthay of current user + */ + @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC}, max = 1) + public int getDaysTillMyBirthday() { + return CustomCache.getDaysTillMyBirthday((Void) -> mService.getDaysTillMyBirthday()); + } + + /** + * Testing custom class values to generate static IpcDataCache with max capasity of 1 and custom + * api + * + * @return number of days since birthay of current user + */ + @CachedProperty(modsFlagOnOrNone = {}, max = 1, api = "my_unique_key") + public int getDaysSinceMyBirthday() { + return ((CustomCache) mCache).getDaysSinceMyBirthday( + (Void) -> mService.getDaysSinceMyBirthday()); + } + + /** + * Testing custom class values to generate static IpcDataCache with custom module name + * + * @return birthday wishes of given user Id + */ + @CachedProperty(module = "telephony") + public String getBirthdayWishesFromUser(int userId) { + return CustomCache.getBirthdayWishesFromUser(mService::getBirthdayWishesFromUser, + userId); + } + + class BirthdayManagerService { + int mDaysTillBirthday = 182; + + public Date getBirthday(int userId) { + return new Date(2024, 6, 1 + userId); + } + + public int getDaysTillBirthday(int userId) { + return mDaysTillBirthday + userId; + } + + public int getDaysSinceBirthday(int userId) { + return 365 - getDaysTillBirthday(userId); + } + + public int getDaysTillMyBirthday() { + return 0; + } + + public int getDaysSinceMyBirthday() { + return 365; + } + + public String getBirthdayWishesFromUser(int userId) { + return "Happy Birthday!\n- " + userId; + } + } +} diff --git a/tools/processors/property_cache/test/resources/CustomCache.java b/tools/processors/property_cache/test/resources/CustomCache.java new file mode 100644 index 000000000000..326467fb5639 --- /dev/null +++ b/tools/processors/property_cache/test/resources/CustomCache.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.processor.property_cache.test; + +import android.os.IpcDataCache; + +/** + * This class is auto-generated + * + * @hide + **/ +public class CustomCache { + private static final Object sBirthdayLock = new Object(); + private static IpcDataCache<java.lang.Integer, java.util.Date> sBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Custom#getBirthday } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public static java.util.Date getBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) { + if (sBirthday != null) { + return sBirthday.query(query); + } + synchronized (sBirthdayLock) { + if (sBirthday == null) { + sBirthday = new IpcDataCache( + new IpcDataCache.Config(4, "bluetooth", "custom_birthday", "Birthday"), + binderCall, bypassPredicate); + + } + } + return sBirthday.query(query); + } + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Custom#getBirthday } + * @param query - parameter to call remote lambda + * @hide + */ + public static java.util.Date getBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall, + java.lang.Integer query) { + if (sBirthday != null) { + return sBirthday.query(query); + } + synchronized (sBirthdayLock) { + if (sBirthday == null) { + sBirthday = new IpcDataCache( + new IpcDataCache.Config(4, "bluetooth", "custom_birthday", "Birthday"), + binderCall); + } + } + return sBirthday.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Custom#getBirthday} + * + * @hide + */ + public static final void invalidateBirthday() { + IpcDataCache.invalidateCache("bluetooth", "custom_birthday"); + } + + private static final Object sDaysTillBirthdayLock = new Object(); + private static IpcDataCache<java.lang.Integer, java.lang.Integer> sDaysTillBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Custom#getDaysTillBirthday } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.Integer getDaysTillBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) { + if (sDaysTillBirthday != null) { + return sDaysTillBirthday.query(query); + } + synchronized (sDaysTillBirthdayLock) { + if (sDaysTillBirthday == null) { + sDaysTillBirthday = new IpcDataCache( + new IpcDataCache.Config(4, "bluetooth", "custom_days_till_birthday", + "DaysTillBirthday"), binderCall, bypassPredicate); + + } + } + return sDaysTillBirthday.query(query); + } + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Custom#getDaysTillBirthday } + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.Integer getDaysTillBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + java.lang.Integer query) { + if (sDaysTillBirthday != null) { + return sDaysTillBirthday.query(query); + } + synchronized (sDaysTillBirthdayLock) { + if (sDaysTillBirthday == null) { + sDaysTillBirthday = new IpcDataCache( + new IpcDataCache.Config(4, "bluetooth", "custom_days_till_birthday", + "DaysTillBirthday"), binderCall); + + } + } + return sDaysTillBirthday.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Custom#getDaysTillBirthday} + * + * @hide + */ + public static final void invalidateDaysTillBirthday() { + IpcDataCache.invalidateCache("bluetooth", "custom_days_till_birthday"); + } + + private final Object mDaysSinceBirthdayLock = new Object(); + private IpcDataCache<java.lang.Integer, java.lang.Integer> mDaysSinceBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Custom#getDaysSinceBirthday } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public java.lang.Integer getDaysSinceBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) { + if (mDaysSinceBirthday != null) { + return mDaysSinceBirthday.query(query); + } + synchronized (mDaysSinceBirthdayLock) { + if (mDaysSinceBirthday == null) { + mDaysSinceBirthday = new IpcDataCache( + new IpcDataCache.Config(4, "bluetooth", "custom_days_since_birthday", + "DaysSinceBirthday"), binderCall, bypassPredicate); + + } + } + return mDaysSinceBirthday.query(query); + } + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Custom#getDaysSinceBirthday + * } + * @param query - parameter to call remote lambda + * @hide + */ + public java.lang.Integer getDaysSinceBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + java.lang.Integer query) { + if (mDaysSinceBirthday != null) { + return mDaysSinceBirthday.query(query); + } + synchronized (mDaysSinceBirthdayLock) { + if (mDaysSinceBirthday == null) { + mDaysSinceBirthday = new IpcDataCache( + new IpcDataCache.Config(4, "bluetooth", "custom_days_since_birthday", + "DaysSinceBirthday"), binderCall); + + } + } + return mDaysSinceBirthday.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Custom#getDaysSinceBirthday} + * + * @hide + */ + public static final void invalidateDaysSinceBirthday() { + IpcDataCache.invalidateCache("bluetooth", "custom_days_since_birthday"); + } + + private static final Object sDaysTillMyBirthdayLock = new Object(); + private static IpcDataCache<java.lang.Void, java.lang.Integer> sDaysTillMyBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Custom#getDaysTillMyBirthday + * } + * @hide + */ + public static java.lang.Integer getDaysTillMyBirthday( + IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall) { + if (sDaysTillMyBirthday != null) { + return sDaysTillMyBirthday.query(null); + } + synchronized (sDaysTillMyBirthdayLock) { + if (sDaysTillMyBirthday == null) { + sDaysTillMyBirthday = new IpcDataCache( + new IpcDataCache.Config(1, "bluetooth", "custom_days_till_my_birthday", + "DaysTillMyBirthday"), binderCall); + + } + } + return sDaysTillMyBirthday.query(null); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Custom#getDaysTillMyBirthday} + * + * @hide + */ + public static final void invalidateDaysTillMyBirthday() { + IpcDataCache.invalidateCache("bluetooth", "custom_days_till_my_birthday"); + } + + private final Object mDaysSinceMyBirthdayLock = new Object(); + private IpcDataCache<java.lang.Void, java.lang.Integer> mDaysSinceMyBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Custom#getDaysSinceMyBirthday + * } + * @hide + */ + public java.lang.Integer getDaysSinceMyBirthday( + IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall) { + if (mDaysSinceMyBirthday != null) { + return mDaysSinceMyBirthday.query(null); + } + synchronized (mDaysSinceMyBirthdayLock) { + if (mDaysSinceMyBirthday == null) { + mDaysSinceMyBirthday = new IpcDataCache( + new IpcDataCache.Config(1, "bluetooth", "my_unique_key", + "DaysSinceMyBirthday"), binderCall); + + } + } + return mDaysSinceMyBirthday.query(null); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Custom#getDaysSinceMyBirthday} + * + * @hide + */ + public static final void invalidateDaysSinceMyBirthday() { + IpcDataCache.invalidateCache("bluetooth", "my_unique_key"); + } + + private static final Object sBirthdayWishesFromUserLock = new Object(); + private static IpcDataCache<java.lang.Integer, java.lang.String> sBirthdayWishesFromUser; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Custom#getBirthdayWishesFromUser + * } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.String getBirthdayWishesFromUser( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) { + if (sBirthdayWishesFromUser != null) { + return sBirthdayWishesFromUser.query(query); + } + synchronized (sBirthdayWishesFromUserLock) { + if (sBirthdayWishesFromUser == null) { + sBirthdayWishesFromUser = new IpcDataCache( + new IpcDataCache.Config(4, "telephony", "custom_birthday_wishes_from_user", + "BirthdayWishesFromUser"), binderCall, bypassPredicate); + + } + } + return sBirthdayWishesFromUser.query(query); + } + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Custom#getBirthdayWishesFromUser } + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.String getBirthdayWishesFromUser( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall, + java.lang.Integer query) { + if (sBirthdayWishesFromUser != null) { + return sBirthdayWishesFromUser.query(query); + } + synchronized (sBirthdayWishesFromUserLock) { + if (sBirthdayWishesFromUser == null) { + sBirthdayWishesFromUser = new IpcDataCache( + new IpcDataCache.Config(4, "telephony", "custom_birthday_wishes_from_user", + "BirthdayWishesFromUser"), binderCall); + + } + } + return sBirthdayWishesFromUser.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Custom#getBirthdayWishesFromUser} + * + * @hide + */ + public static final void invalidateBirthdayWishesFromUser() { + IpcDataCache.invalidateCache("telephony", "custom_birthday_wishes_from_user"); + } + + + /** + * This method is auto-generated - initialise all caches for class CustomCache + * + * @hide + */ + public static void initCache() { + CustomCache.invalidateBirthday(); + CustomCache.invalidateDaysTillBirthday(); + CustomCache.invalidateDaysSinceBirthday(); + CustomCache.invalidateDaysTillMyBirthday(); + CustomCache.invalidateDaysSinceMyBirthday(); + CustomCache.invalidateBirthdayWishesFromUser(); + } +} diff --git a/tools/processors/property_cache/test/resources/Default.java b/tools/processors/property_cache/test/resources/Default.java new file mode 100644 index 000000000000..d2449aad656c --- /dev/null +++ b/tools/processors/property_cache/test/resources/Default.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.processor.property_cache.test; + +import com.android.internal.annotations.CacheModifier; +import com.android.internal.annotations.CachedProperty; +import com.android.internal.annotations.CachedPropertyDefaults; + +import java.util.Date; + +@CachedPropertyDefaults() +public class Default { + BirthdayManagerService mService = new BirthdayManagerService(); + Object mCache = new DefaultCache(); + + /** Testing default class values to generate static IpcDataCache + * + * @param userId - user Id + * @return birthday date of given user Id + */ + @CachedProperty() + public Date getBirthday(int userId) { + return DefaultCache.getBirthday(mService::getBirthday, userId); + } + + /** Testing default class values to generate static IpcDataCache + * + * @param userId - user Id + * @return number of days till birthday of given user Id + */ + @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC}) + public int getDaysTillBirthday(int userId) { + return DefaultCache.getDaysTillBirthday(mService::getDaysTillBirthday, userId); + } + + /** Testing generate non-static IpcDataCache + * + * @param userId - user Id + * @return number of days since birthday of given user Id + */ + @CachedProperty(modsFlagOnOrNone = {}) + public int getDaysSinceBirthday(int userId) { + return ((DefaultCache) mCache).getDaysSinceBirthday(mService::getDaysSinceBirthday, userId); + } + + /** Testing default class values to generate static IpcDataCache with max capacity of 1 + * + * @return number of days till birthay of current user + */ + @CachedProperty( + modsFlagOnOrNone = {CacheModifier.STATIC}, + max = 1) + public int getDaysTillMyBirthday() { + return DefaultCache.getDaysTillMyBirthday((Void) -> mService.getDaysTillMyBirthday()); + } + + /** Testing default class values to generate static IpcDataCache with max capacity of 1 and + custom api + * + * @return number of days since birthay of current user + */ + @CachedProperty( + modsFlagOnOrNone = {}, + max = 1, + api = "my_unique_key") + public int getDaysSinceMyBirthday() { + return ((DefaultCache) mCache).getDaysSinceMyBirthday( + (Void) -> mService.getDaysSinceMyBirthday()); + } + + /** Testing default class values to generate static IpcDataCache with custom module name + * + * @return birthday wishes of given user Id + */ + @CachedProperty(module = "telephony") + public String getBirthdayWishesFromUser(int userId) { + return DefaultCache.getBirthdayWishesFromUser(mService::getBirthdayWishesFromUser, + userId); + } + + class BirthdayManagerService { + + BirthdayManagerService() { + DefaultCache.initCache(); + } + + public Date getBirthday(int userId) { + return new Date(); + } + + public int getDaysTillBirthday(int userId) { + return 0; + } + + public int getDaysSinceBirthday(int userId) { + return 0; + } + + public int getDaysTillMyBirthday() { + return 0; + } + + public int getDaysSinceMyBirthday() { + return 0; + } + + public String getBirthdayWishesFromUser(int userId) { + return "Happy Birthday!\n- " + userId; + } + } +} diff --git a/tools/processors/property_cache/test/resources/DefaultCache.java b/tools/processors/property_cache/test/resources/DefaultCache.java new file mode 100644 index 000000000000..9531118752bb --- /dev/null +++ b/tools/processors/property_cache/test/resources/DefaultCache.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.processor.property_cache.test; + +import android.os.IpcDataCache; + +/** + * This class is auto-generated + * + * @hide + **/ +public class DefaultCache { + private static final Object sBirthdayLock = new Object(); + private static IpcDataCache<java.lang.Integer, java.util.Date> sBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Default#getBirthday } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public static java.util.Date getBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query + ) { + if (sBirthday != null) { + return sBirthday.query(query); + } + synchronized (sBirthdayLock) { + if (sBirthday == null) { + sBirthday = new IpcDataCache( + new IpcDataCache.Config(32, "system_server", + "default_birthday", "Birthday"), + binderCall, bypassPredicate); + + } + } + return sBirthday.query(query); + } + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Default#getBirthday } + * @param query - parameter to call remote lambda + * @hide + */ + public static java.util.Date getBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall, + java.lang.Integer query + ) { + if (sBirthday != null) { + return sBirthday.query(query); + } + synchronized (sBirthdayLock) { + if (sBirthday == null) { + sBirthday = new IpcDataCache( + new IpcDataCache.Config(32, "system_server", + "default_birthday", "Birthday"), + binderCall); + + } + } + return sBirthday.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Default#getBirthday} + * + * @hide + */ + public static final void invalidateBirthday() { + IpcDataCache.invalidateCache("system_server", "default_birthday"); + } + + private static final Object sDaysTillBirthdayLock = new Object(); + private static IpcDataCache<java.lang.Integer, java.lang.Integer> sDaysTillBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Default#getDaysTillBirthday } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.Integer getDaysTillBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query + ) { + if (sDaysTillBirthday != null) { + return sDaysTillBirthday.query(query); + } + synchronized (sDaysTillBirthdayLock) { + if (sDaysTillBirthday == null) { + sDaysTillBirthday = new IpcDataCache( + new IpcDataCache.Config(32, "system_server", "default_days_till_birthday", + "DaysTillBirthday"), binderCall, bypassPredicate); + + } + } + return sDaysTillBirthday.query(query); + } + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Default#getDaysTillBirthday + * } + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.Integer getDaysTillBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + java.lang.Integer query + ) { + if (sDaysTillBirthday != null) { + return sDaysTillBirthday.query(query); + } + synchronized (sDaysTillBirthdayLock) { + if (sDaysTillBirthday == null) { + sDaysTillBirthday = new IpcDataCache( + new IpcDataCache.Config(32, "system_server", "default_days_till_birthday", + "DaysTillBirthday"), binderCall); + + } + } + return sDaysTillBirthday.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Default#getDaysTillBirthday} + * + * @hide + */ + public static final void invalidateDaysTillBirthday() { + IpcDataCache.invalidateCache("system_server", "default_days_till_birthday"); + } + + private final Object mDaysSinceBirthdayLock = new Object(); + private IpcDataCache<java.lang.Integer, java.lang.Integer> mDaysSinceBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Default#getDaysSinceBirthday } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public java.lang.Integer getDaysSinceBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query + ) { + if (mDaysSinceBirthday != null) { + return mDaysSinceBirthday.query(query); + } + synchronized (mDaysSinceBirthdayLock) { + if (mDaysSinceBirthday == null) { + mDaysSinceBirthday = new IpcDataCache( + new IpcDataCache.Config(32, "system_server", "default_days_since_birthday", + "DaysSinceBirthday"), binderCall, bypassPredicate); + + } + } + return mDaysSinceBirthday.query(query); + } + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Default#getDaysSinceBirthday + * } + * @param query - parameter to call remote lambda + * @hide + */ + public java.lang.Integer getDaysSinceBirthday( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall, + java.lang.Integer query + ) { + if (mDaysSinceBirthday != null) { + return mDaysSinceBirthday.query(query); + } + synchronized (mDaysSinceBirthdayLock) { + if (mDaysSinceBirthday == null) { + mDaysSinceBirthday = new IpcDataCache( + new IpcDataCache.Config(32, "system_server", "default_days_since_birthday", + "DaysSinceBirthday"), binderCall); + + } + } + return mDaysSinceBirthday.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Default#getDaysSinceBirthday} + * + * @hide + */ + public static final void invalidateDaysSinceBirthday() { + IpcDataCache.invalidateCache("system_server", "default_days_since_birthday"); + } + + private static final Object sDaysTillMyBirthdayLock = new Object(); + private static IpcDataCache<java.lang.Void, java.lang.Integer> sDaysTillMyBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link android.processor.property_cache.test.Default#getDaysTillMyBirthday + * } + * @hide + */ + public static java.lang.Integer getDaysTillMyBirthday( + IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall + ) { + if (sDaysTillMyBirthday != null) { + return sDaysTillMyBirthday.query(null); + } + synchronized (sDaysTillMyBirthdayLock) { + if (sDaysTillMyBirthday == null) { + sDaysTillMyBirthday = new IpcDataCache( + new IpcDataCache.Config(1, "system_server", "default_days_till_my_birthday", + "DaysTillMyBirthday"), binderCall); + + } + } + return sDaysTillMyBirthday.query(null); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Default#getDaysTillMyBirthday} + * + * @hide + */ + public static final void invalidateDaysTillMyBirthday() { + IpcDataCache.invalidateCache("system_server", "default_days_till_my_birthday"); + } + + private final Object mDaysSinceMyBirthdayLock = new Object(); + private IpcDataCache<java.lang.Void, java.lang.Integer> mDaysSinceMyBirthday; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Default#getDaysSinceMyBirthday } + * @hide + */ + public java.lang.Integer getDaysSinceMyBirthday( + IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall + ) { + if (mDaysSinceMyBirthday != null) { + return mDaysSinceMyBirthday.query(null); + } + synchronized (mDaysSinceMyBirthdayLock) { + if (mDaysSinceMyBirthday == null) { + mDaysSinceMyBirthday = new IpcDataCache( + new IpcDataCache.Config(1, "system_server", "my_unique_key", + "DaysSinceMyBirthday"), binderCall); + + } + } + return mDaysSinceMyBirthday.query(null); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Default#getDaysSinceMyBirthday} + * + * @hide + */ + public static final void invalidateDaysSinceMyBirthday() { + IpcDataCache.invalidateCache("system_server", "my_unique_key"); + } + + private static final Object sBirthdayWishesFromUserLock = new Object(); + private static IpcDataCache<java.lang.Integer, java.lang.String> sBirthdayWishesFromUser; + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * + * android.processor.property_cache.test.Default#getBirthdayWishesFromUser + * } + * @param bypassPredicate - lambda to bypass remote call + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.String getBirthdayWishesFromUser( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall, + IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query + ) { + if (sBirthdayWishesFromUser != null) { + return sBirthdayWishesFromUser.query(query); + } + synchronized (sBirthdayWishesFromUserLock) { + if (sBirthdayWishesFromUser == null) { + sBirthdayWishesFromUser = new IpcDataCache( + new IpcDataCache.Config(32, "telephony", + "default_birthday_wishes_from_user", + "BirthdayWishesFromUser"), binderCall, bypassPredicate); + + } + } + return sBirthdayWishesFromUser.query(query); + } + + + /** + * This method is auto-generated + * + * @param binderCall - lambda for remote call + * {@link + * android.processor.property_cache.test.Default#getBirthdayWishesFromUser } + * @param query - parameter to call remote lambda + * @hide + */ + public static java.lang.String getBirthdayWishesFromUser( + IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall, + java.lang.Integer query + ) { + if (sBirthdayWishesFromUser != null) { + return sBirthdayWishesFromUser.query(query); + } + synchronized (sBirthdayWishesFromUserLock) { + if (sBirthdayWishesFromUser == null) { + sBirthdayWishesFromUser = new IpcDataCache( + new IpcDataCache.Config(32, "telephony", + "default_birthday_wishes_from_user", + "BirthdayWishesFromUser"), binderCall); + + } + } + return sBirthdayWishesFromUser.query(query); + } + + /** + * This method is auto-generated- invalidate cache for + * {@link android.processor.property_cache.test.Default#getBirthdayWishesFromUser} + * + * @hide + */ + public static final void invalidateBirthdayWishesFromUser() { + IpcDataCache.invalidateCache("telephony", "default_birthday_wishes_from_user"); + } + + + /** + * This method is auto-generated - initialise all caches for class DefaultCache + * + * @hide + */ + public static void initCache() { + DefaultCache.invalidateBirthday(); + DefaultCache.invalidateDaysTillBirthday(); + DefaultCache.invalidateDaysSinceBirthday(); + DefaultCache.invalidateDaysTillMyBirthday(); + DefaultCache.invalidateDaysSinceMyBirthday(); + DefaultCache.invalidateBirthdayWishesFromUser(); + } +} diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt index c3595b7ac288..272d8bb1793d 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt @@ -119,7 +119,8 @@ class ProtoLogCallProcessorImpl( } logCallVisitor?.processCall(call, messageString, getLevelForMethodName( - call.name.toString(), call, context), groupMap.getValue(groupName)) + call.name.toString(), call, context), groupMap.getValue(groupName), + context.lineNumber) } else if (call.name.id == "init") { // No processing } else { diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt index 8cd927a7cd0e..216241ac5a47 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt @@ -20,5 +20,11 @@ import com.android.internal.protolog.common.LogLevel import com.github.javaparser.ast.expr.MethodCallExpr interface ProtoLogCallVisitor { - fun processCall(call: MethodCallExpr, messageString: String, level: LogLevel, group: LogGroup) + fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup, + lineNumber: Int + ) } diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt index 22858803b597..d8a2954545bb 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt @@ -16,6 +16,7 @@ package com.android.protolog.tool +import com.android.internal.protolog.common.IProtoLog import com.android.internal.protolog.common.LogLevel import com.android.internal.protolog.common.ProtoLogToolInjected import com.android.protolog.tool.CommandOptions.Companion.USAGE @@ -68,7 +69,8 @@ object ProtoLogTool { val messageString: String, val logLevel: LogLevel, val logGroup: LogGroup, - val position: String + val position: String, + val lineNumber: Int, ) private fun showHelpAndExit() { @@ -319,6 +321,7 @@ object ProtoLogTool { MethodCallExpr() .setName("isEnabled") .setArguments(NodeList( + NameExpr("protoLogInstance"), FieldAccessExpr() .setScope(NameExpr(protoLogGroupsClassName)) .setName(group.value.name), @@ -332,6 +335,7 @@ object ProtoLogTool { } cacheClass.addMethod("update").setPrivate(true).setStatic(true) + .addParameter(IProtoLog::class.java, "protoLogInstance") .setBody(updateBlockStmt) classDeclaration.addMember(cacheClass) @@ -368,7 +372,7 @@ object ProtoLogTool { } interface ProtologViewerConfigBuilder { - fun build(statements: Map<LogCall, Long>): ByteArray + fun build(groups: Collection<LogGroup>, statements: Map<LogCall, Long>): ByteArray } private fun viewerConf(command: CommandOptions) { @@ -416,7 +420,7 @@ object ProtoLogTool { } val outFile = injector.fileOutputStream(command.viewerConfigFileNameArg) - outFile.write(configBuilder.build(logCallRegistry.getStatements())) + outFile.write(configBuilder.build(groups.values, logCallRegistry.getStatements())) outFile.close() } @@ -432,9 +436,10 @@ object ProtoLogTool { call: MethodCallExpr, messageString: String, level: LogLevel, - group: LogGroup + group: LogGroup, + lineNumber: Int, ) { - val logCall = LogCall(messageString, level, group, packagePath) + val logCall = LogCall(messageString, level, group, packagePath, lineNumber) calls.add(logCall) } } diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt index c478f5844de6..76df0674df65 100644 --- a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt +++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt @@ -91,7 +91,8 @@ class SourceTransformer( call: MethodCallExpr, messageString: String, level: LogLevel, - group: LogGroup + group: LogGroup, + lineNumber: Int, ) { validateCall(call) val processedCallStatement = diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt index 7714db212c9f..16a3d7cdda21 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt @@ -21,8 +21,7 @@ import com.android.protolog.tool.Constants.VERSION import java.io.StringWriter class ViewerConfigJsonBuilder : ProtoLogTool.ProtologViewerConfigBuilder { - override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { - val groups = statements.map { it.key.logGroup }.toSet() + override fun build(groups: Collection<LogGroup>, statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { val stringWriter = StringWriter() val writer = JsonWriter(stringWriter) writer.setIndent(" ") diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt index 245e802df49b..5af2d9440533 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt @@ -28,10 +28,14 @@ class ViewerConfigProtoBuilder : ProtoLogTool.ProtologViewerConfigBuilder { * @return a byte array of a ProtoLogViewerConfig proto message encoding all the viewer * configurations mapping protolog hashes to message information and log group information. */ - override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { + override fun build(groups: Collection<LogGroup>, statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { val configBuilder = ProtoLogViewerConfig.newBuilder() - val groups = statements.map { it.key.logGroup }.toSet() + // TODO(b/373754057): We are passing all the groups now, because some groups might only be + // used by Kotlin code that is not processed, but for group that get enabled to log to + // logcat we try and load the viewer configurations for this group, so the group must exist + // in the viewer config. Once Kotlin is pre-processed or this logic changes we should only + // use the groups that are actually used as an optimization. val groupIds = mutableMapOf<LogGroup, Int>() groups.forEach { groupIds.putIfAbsent(it, groupIds.size + 1) @@ -55,7 +59,7 @@ class ViewerConfigProtoBuilder : ProtoLogTool.ProtologViewerConfigBuilder { .setLevel( ProtoLogLevel.forNumber(log.logLevel.id)) .setGroupId(groupId) - .setLocation(log.position) + .setLocation("${log.position}:${log.lineNumber}") ) } diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt index 5e50f71d75cc..004d97babbad 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt @@ -42,7 +42,8 @@ class ProtoLogCallProcessorImplTest { call: MethodCallExpr, messageString: String, level: LogLevel, - group: LogGroup + group: LogGroup, + lineNumber: Int, ) { calls.add(LogCall(call, messageString, level, group)) } diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt index 6cde7a72db53..674a907d68d9 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt @@ -158,7 +158,7 @@ class SourceTransformerTest { 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")) + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"), 123) invocation.arguments[0] as CompilationUnit } @@ -195,11 +195,11 @@ class SourceTransformerTest { val calls = code.findAll(MethodCallExpr::class.java) visitor.processCall(calls[0], "test %d %f", - LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"), 456) visitor.processCall(calls[1], "test %d %f", - LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"), 789) visitor.processCall(calls[2], "test %d %f", - LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST")) + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"), 123) invocation.arguments[0] as CompilationUnit } @@ -236,7 +236,7 @@ class SourceTransformerTest { visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", - true, true, "WM_TEST")) + true, true, "WM_TEST"), 123) invocation.arguments[0] as CompilationUnit } @@ -273,7 +273,7 @@ class SourceTransformerTest { 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")) + LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"), 456) invocation.arguments[0] as CompilationUnit } @@ -307,7 +307,7 @@ class SourceTransformerTest { 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")) + LogLevel.WARN, LogGroup("TEST_GROUP", true, false, "WM_TEST"), 789) invocation.arguments[0] as CompilationUnit } @@ -344,7 +344,7 @@ class SourceTransformerTest { visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP", - true, false, "WM_TEST")) + true, false, "WM_TEST"), 123) invocation.arguments[0] as CompilationUnit } diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt index d27ae88fc488..bcbc8790e170 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt @@ -36,6 +36,8 @@ class ViewerConfigJsonBuilderTest { 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 GROUPS = listOf(GROUP1, GROUP2, GROUP3) } private val configBuilder = ViewerConfigJsonBuilder() @@ -48,12 +50,12 @@ class ViewerConfigJsonBuilderTest { fun processClass() { val logCallRegistry = ProtoLogTool.LogCallRegistry() logCallRegistry.addLogCalls(listOf( - LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), - LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP2, PATH), - LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH))) + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH, 123), + LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP2, PATH, 456), + LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH, 789))) val parsedConfig = parseConfig( - configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8)) + configBuilder.build(GROUPS, logCallRegistry.getStatements()).toString(Charsets.UTF_8)) assertEquals(3, parsedConfig.size) assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString, LogLevel.INFO, GROUP1)]) @@ -67,12 +69,12 @@ class ViewerConfigJsonBuilderTest { fun processClass_nonUnique() { val logCallRegistry = ProtoLogTool.LogCallRegistry() logCallRegistry.addLogCalls(listOf( - LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), - LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), - LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH))) + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH, 123), + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH, 456), + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH, 789))) val parsedConfig = parseConfig( - configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8)) + configBuilder.build(GROUPS, logCallRegistry.getStatements()).toString(Charsets.UTF_8)) assertEquals(1, parsedConfig.size) assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString, LogLevel.INFO, GROUP1)]) diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigProtoBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigProtoBuilderTest.kt new file mode 100644 index 000000000000..dfc66405eec9 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigProtoBuilderTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.internal.protolog.common.LogLevel +import com.android.protolog.tool.ProtoLogTool.LogCall +import com.google.common.truth.Truth +import org.junit.Test +import perfetto.protos.PerfettoTrace.ProtoLogViewerConfig + +class ViewerConfigProtoBuilderTest { + 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 GROUP1 = LogGroup("TEST_GROUP", true, true, TAG1) + private val GROUP2 = LogGroup("DEBUG_GROUP", true, true, TAG2) + private val GROUP3 = LogGroup("UNUSED_GROUP", true, true, TAG1) + + private val GROUPS = listOf( + GROUP1, + GROUP2, + GROUP3 + ) + + private const val PATH = "/tmp/test.java" + } + + @Test + fun includesUnusedProtoLogGroups() { + // Added because of b/373754057. This test might need to be removed in the future. + + val configBuilder = ViewerConfigProtoBuilder() + + val logCallRegistry = ProtoLogTool.LogCallRegistry() + logCallRegistry.addLogCalls(listOf( + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH, 123), + LogCall(TEST2.messageString, LogLevel.INFO, GROUP2, PATH, 456), + )) + + val rawProto = configBuilder.build(GROUPS, logCallRegistry.getStatements()) + + val viewerConfig = ProtoLogViewerConfig.parseFrom(rawProto) + Truth.assertThat(viewerConfig.groupsCount).isEqualTo(GROUPS.size) + Truth.assertThat(viewerConfig.messagesCount).isLessThan(GROUPS.size) + } + + @Test + fun includesLineNumberInLocation() { + val configBuilder = ViewerConfigProtoBuilder() + + val logCallRegistry = ProtoLogTool.LogCallRegistry() + logCallRegistry.addLogCalls(listOf( + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH, 123), + LogCall(TEST2.messageString, LogLevel.INFO, GROUP2, PATH, 456), + )) + + val rawProto = configBuilder.build(GROUPS, logCallRegistry.getStatements()) + + val viewerConfig = ProtoLogViewerConfig.parseFrom(rawProto) + + Truth.assertThat(viewerConfig.messagesList[0].location).isEqualTo("$PATH:123") + Truth.assertThat(viewerConfig.messagesList[1].location).isEqualTo("$PATH:456") + } +}
\ No newline at end of file |