summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/aapt2/Debug.cpp92
-rw-r--r--tools/aapt2/Debug.h1
-rw-r--r--tools/aapt2/Main.cpp6
-rw-r--r--tools/aapt2/cmd/Compile.cpp12
-rw-r--r--tools/aapt2/cmd/Dump.cpp11
-rw-r--r--tools/aapt2/cmd/Dump.h12
-rw-r--r--tools/aapt2/cmd/Optimize.cpp50
-rw-r--r--tools/aapt2/cmd/Optimize.h15
-rw-r--r--tools/aapt2/cmd/Optimize_test.cpp68
-rw-r--r--tools/aapt2/format/binary/TableFlattener.cpp14
-rw-r--r--tools/aapt2/format/binary/TableFlattener.h5
-rw-r--r--tools/aapt2/format/binary/TableFlattener_test.cpp15
-rw-r--r--tools/aapt2/link/XmlReferenceLinker.cpp9
-rw-r--r--tools/aapt2/link/XmlReferenceLinker_test.cpp63
-rw-r--r--tools/aapt2/test/Context.h4
-rw-r--r--tools/codegen/src/com/android/codegen/ClassInfo.kt5
-rw-r--r--tools/codegen/src/com/android/codegen/ClassPrinter.kt10
-rw-r--r--tools/codegen/src/com/android/codegen/Generators.kt221
-rwxr-xr-xtools/codegen/src/com/android/codegen/Main.kt17
-rw-r--r--tools/codegen/src/com/android/codegen/SharedConstants.kt2
-rw-r--r--tools/protologtool/Android.bp33
-rw-r--r--tools/protologtool/README.md119
-rw-r--r--tools/protologtool/TEST_MAPPING7
-rw-r--r--tools/protologtool/manifest.txt1
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt81
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt205
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/Constants.kt24
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/LogGroup.kt24
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/LogLevel.kt38
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/LogParser.kt115
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt26
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt113
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt23
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt50
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt140
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt232
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt105
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt122
-rw-r--r--tools/protologtool/src/com/android/protolog/tool/exceptions.kt41
-rw-r--r--tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt182
-rw-r--r--tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt250
-rw-r--r--tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt187
-rw-r--r--tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt226
-rw-r--r--tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt456
-rw-r--r--tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt130
-rw-r--r--tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt327
-rw-r--r--tools/validatekeymaps/Main.cpp1
47 files changed, 3713 insertions, 177 deletions
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index 7ffa5ffc09fe..137fbd671865 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -246,6 +246,36 @@ class ValueBodyPrinter : public ConstValueVisitor {
Printer* printer_;
};
+std::string OverlayablePoliciesToString(OverlayableItem::PolicyFlags policies) {
+ static const std::map<OverlayableItem::PolicyFlags, std::string> kFlagToString = {
+ {OverlayableItem::kPublic, "public"},
+ {OverlayableItem::kSystem, "system"},
+ {OverlayableItem::kVendor, "vendor"},
+ {OverlayableItem::kProduct, "product"},
+ {OverlayableItem::kSignature, "signature"},
+ {OverlayableItem::kOdm, "odm"},
+ {OverlayableItem::kOem, "oem"},
+ };
+ std::string str;
+ for (auto const& policy : kFlagToString) {
+ if ((policies & policy.first) != policy.first) {
+ continue;
+ }
+ if (!str.empty()) {
+ str.append("|");
+ }
+ str.append(policy.second);
+ policies &= ~policy.first;
+ }
+ if (policies != 0) {
+ if (!str.empty()) {
+ str.append("|");
+ }
+ str.append(StringPrintf("0x%08x", policies));
+ }
+ return !str.empty() ? str : "none";
+}
+
} // namespace
void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options,
@@ -312,6 +342,10 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions&
break;
}
+ if (entry->overlayable_item) {
+ printer->Print(" OVERLAYABLE");
+ }
+
printer->Println();
if (options.show_values) {
@@ -525,4 +559,62 @@ void Debug::DumpXml(const xml::XmlResource& doc, Printer* printer) {
doc.root->Accept(&xml_visitor);
}
+struct DumpOverlayableEntry {
+ std::string overlayable_section;
+ std::string policy_subsection;
+ std::string resource_name;
+};
+
+void Debug::DumpOverlayable(const ResourceTable& table, text::Printer* printer) {
+ std::vector<DumpOverlayableEntry> items;
+ for (const auto& package : table.packages) {
+ for (const auto& type : package->types) {
+ for (const auto& entry : type->entries) {
+ if (entry->overlayable_item) {
+ const auto& overlayable_item = entry->overlayable_item.value();
+ const auto overlayable_section = StringPrintf(R"(name="%s" actor="%s")",
+ overlayable_item.overlayable->name.c_str(),
+ overlayable_item.overlayable->actor.c_str());
+ const auto policy_subsection = StringPrintf(R"(policies="%s")",
+ OverlayablePoliciesToString(overlayable_item.policies).c_str());
+ const auto value =
+ StringPrintf("%s/%s", to_string(type->type).data(), entry->name.c_str());
+ items.push_back(DumpOverlayableEntry{overlayable_section, policy_subsection, value});
+ }
+ }
+ }
+ }
+
+ std::sort(items.begin(), items.end(),
+ [](const DumpOverlayableEntry& a, const DumpOverlayableEntry& b) {
+ if (a.overlayable_section != b.overlayable_section) {
+ return a.overlayable_section < b.overlayable_section;
+ }
+ if (a.policy_subsection != b.policy_subsection) {
+ return a.policy_subsection < b.policy_subsection;
+ }
+ return a.resource_name < b.resource_name;
+ });
+
+ std::string last_overlayable_section;
+ std::string last_policy_subsection;
+ for (const auto& item : items) {
+ if (last_overlayable_section != item.overlayable_section) {
+ printer->Println(item.overlayable_section);
+ last_overlayable_section = item.overlayable_section;
+ }
+ if (last_policy_subsection != item.policy_subsection) {
+ printer->Indent();
+ printer->Println(item.policy_subsection);
+ last_policy_subsection = item.policy_subsection;
+ printer->Undent();
+ }
+ printer->Indent();
+ printer->Indent();
+ printer->Println(item.resource_name);
+ printer->Undent();
+ printer->Undent();
+ }
+}
+
} // namespace aapt
diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h
index a43197cacf7b..9443d606d7e5 100644
--- a/tools/aapt2/Debug.h
+++ b/tools/aapt2/Debug.h
@@ -39,6 +39,7 @@ struct Debug {
static void DumpHex(const void* data, size_t len);
static void DumpXml(const xml::XmlResource& doc, text::Printer* printer);
static void DumpResStringPool(const android::ResStringPool* pool, text::Printer* printer);
+ static void DumpOverlayable(const ResourceTable& table, text::Printer* printer);
};
} // namespace aapt
diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp
index 213bdd2372ec..8a43bb4ede35 100644
--- a/tools/aapt2/Main.cpp
+++ b/tools/aapt2/Main.cpp
@@ -169,12 +169,12 @@ int MainImpl(int argc, char** argv) {
aapt::text::Printer printer(&fout);
aapt::StdErrDiagnostics diagnostics;
- auto main_command = new aapt::MainCommand(&printer, &diagnostics);
+ aapt::MainCommand main_command(&printer, &diagnostics);
// Add the daemon subcommand here so it cannot be called while executing the daemon
- main_command->AddOptionalSubcommand(
+ main_command.AddOptionalSubcommand(
aapt::util::make_unique<aapt::DaemonCommand>(&fout, &diagnostics));
- return main_command->Execute(args, &std::cerr);
+ return main_command.Execute(args, &std::cerr);
}
int main(int argc, char** argv) {
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index c7ac438dacfb..d50b1de335fb 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -740,7 +740,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) {
}
std::unique_ptr<io::IFileCollection> file_collection;
- std::unique_ptr<IArchiveWriter> archive_writer;
// Collect the resources files to compile
if (options_.res_dir && options_.res_zip) {
@@ -761,8 +760,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) {
context.GetDiagnostics()->Error(DiagMessage(options_.res_dir.value()) << err);
return 1;
}
-
- archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path);
} else if (options_.res_zip) {
if (!args.empty()) {
context.GetDiagnostics()->Error(DiagMessage() << "files given but --zip specified");
@@ -777,8 +774,6 @@ int CompileCommand::Action(const std::vector<std::string>& args) {
context.GetDiagnostics()->Error(DiagMessage(options_.res_zip.value()) << err);
return 1;
}
-
- archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path);
} else {
auto collection = util::make_unique<io::FileCollection>();
@@ -791,7 +786,14 @@ int CompileCommand::Action(const std::vector<std::string>& args) {
}
file_collection = std::move(collection);
+ }
+
+ std::unique_ptr<IArchiveWriter> archive_writer;
+ file::FileType output_file_type = file::GetFileType(options_.output_path);
+ if (output_file_type == file::FileType::kDirectory) {
archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options_.output_path);
+ } else {
+ archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path);
}
if (!archive_writer) {
diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp
index 429aff1ff594..3982d12f6036 100644
--- a/tools/aapt2/cmd/Dump.cpp
+++ b/tools/aapt2/cmd/Dump.cpp
@@ -394,6 +394,17 @@ int DumpXmlTreeCommand::Dump(LoadedApk* apk) {
return 0;
}
+int DumpOverlayableCommand::Dump(LoadedApk* apk) {
+ ResourceTable* table = apk->GetResourceTable();
+ if (!table) {
+ GetDiagnostics()->Error(DiagMessage() << "Failed to retrieve resource table");
+ return 1;
+ }
+
+ Debug::DumpOverlayable(*table, GetPrinter());
+ return 0;
+}
+
const char DumpBadgerCommand::kBadgerData[2925] = {
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 95, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
diff --git a/tools/aapt2/cmd/Dump.h b/tools/aapt2/cmd/Dump.h
index 7ded9bcf8470..cd51f7a7718c 100644
--- a/tools/aapt2/cmd/Dump.h
+++ b/tools/aapt2/cmd/Dump.h
@@ -240,6 +240,16 @@ class DumpXmlTreeCommand : public DumpApkCommand {
std::vector<std::string> files_;
};
+class DumpOverlayableCommand : public DumpApkCommand {
+ public:
+ explicit DumpOverlayableCommand(text::Printer* printer, IDiagnostics* diag)
+ : DumpApkCommand("overlayable", printer, diag) {
+ SetDescription("Print the <overlayable> resources of an APK.");
+ }
+
+ int Dump(LoadedApk* apk) override;
+};
+
/** The default dump command. Performs no action because a subcommand is required. */
class DumpCommand : public Command {
public:
@@ -255,8 +265,8 @@ class DumpCommand : public Command {
AddOptionalSubcommand(util::make_unique<DumpTableCommand>(printer, diag_));
AddOptionalSubcommand(util::make_unique<DumpXmlStringsCommand>(printer, diag_));
AddOptionalSubcommand(util::make_unique<DumpXmlTreeCommand>(printer, diag_));
+ AddOptionalSubcommand(util::make_unique<DumpOverlayableCommand>(printer, diag_));
AddOptionalSubcommand(util::make_unique<DumpBadgerCommand>(printer), /* hidden */ true);
- // TODO(b/120609160): Add aapt2 overlayable dump command
}
int Action(const std::vector<std::string>& args) override {
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 5e06818d7a13..e36668e5a043 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -53,9 +53,9 @@ using ::android::ConfigDescription;
using ::android::ResTable_config;
using ::android::StringPiece;
using ::android::base::ReadFileToString;
-using ::android::base::WriteStringToFile;
using ::android::base::StringAppendF;
using ::android::base::StringPrintf;
+using ::android::base::WriteStringToFile;
namespace aapt {
@@ -300,29 +300,7 @@ class Optimizer {
OptimizeContext* context_;
};
-bool ExtractObfuscationWhitelistFromConfig(const std::string& path, OptimizeContext* context,
- OptimizeOptions* options) {
- std::string contents;
- if (!ReadFileToString(path, &contents, true)) {
- context->GetDiagnostics()->Error(DiagMessage()
- << "failed to parse whitelist from config file: " << path);
- return false;
- }
- for (StringPiece resource_name : util::Tokenize(contents, ',')) {
- options->table_flattener_options.whitelisted_resources.insert(
- resource_name.to_string());
- }
- return true;
-}
-
-bool ExtractConfig(const std::string& path, OptimizeContext* context,
- OptimizeOptions* options) {
- std::string content;
- if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) {
- context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading whitelist");
- return false;
- }
-
+bool ParseConfig(const std::string& content, IAaptContext* context, OptimizeOptions* options) {
size_t line_no = 0;
for (StringPiece line : util::Tokenize(content, '\n')) {
line_no++;
@@ -351,15 +329,24 @@ bool ExtractConfig(const std::string& path, OptimizeContext* context,
for (StringPiece directive : util::Tokenize(directives, ',')) {
if (directive == "remove") {
options->resources_blacklist.insert(resource_name.ToResourceName());
- } else if (directive == "no_obfuscate") {
- options->table_flattener_options.whitelisted_resources.insert(
- resource_name.entry.to_string());
+ } else if (directive == "no_collapse" || directive == "no_obfuscate") {
+ options->table_flattener_options.name_collapse_exemptions.insert(
+ resource_name.ToResourceName());
}
}
}
return true;
}
+bool ExtractConfig(const std::string& path, IAaptContext* context, OptimizeOptions* options) {
+ std::string content;
+ if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) {
+ context->GetDiagnostics()->Error(DiagMessage(path) << "failed reading config file");
+ return false;
+ }
+ return ParseConfig(content, context, options);
+}
+
bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk,
OptimizeOptions* out_options) {
const xml::XmlResource* manifest = apk->GetManifest();
@@ -467,15 +454,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) {
}
}
- if (options_.table_flattener_options.collapse_key_stringpool) {
- if (whitelist_path_) {
- std::string& path = whitelist_path_.value();
- if (!ExtractObfuscationWhitelistFromConfig(path, &context, &options_)) {
- return 1;
- }
- }
- }
-
if (resources_config_path_) {
std::string& path = resources_config_path_.value();
if (!ExtractConfig(path, &context, &options_)) {
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 0be7dad18380..5070ccc8afbf 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -78,10 +78,6 @@ class OptimizeCommand : public Command {
"All the resources that would be unused on devices of the given densities will be \n"
"removed from the APK.",
&target_densities_);
- AddOptionalFlag("--whitelist-path",
- "Path to the whitelist.cfg file containing whitelisted resources \n"
- "whose names should not be altered in final resource tables.",
- &whitelist_path_);
AddOptionalFlag("--resources-config-path",
"Path to the resources.cfg file containing the list of resources and \n"
"directives to each resource. \n"
@@ -104,11 +100,13 @@ class OptimizeCommand : public Command {
"Enables encoding sparse entries using a binary search tree.\n"
"This decreases APK size at the cost of resource retrieval performance.",
&options_.table_flattener_options.use_sparse_entries);
- AddOptionalSwitch("--enable-resource-obfuscation",
- "Enables obfuscation of key string pool to single value",
+ AddOptionalSwitch("--collapse-resource-names",
+ "Collapses resource names to a single value in the key string pool. Resources can \n"
+ "be exempted using the \"no_collapse\" directive in a file specified by "
+ "--resources-config-path.",
&options_.table_flattener_options.collapse_key_stringpool);
- AddOptionalSwitch("--enable-resource-path-shortening",
- "Enables shortening of the path of the resources inside the APK.",
+ AddOptionalSwitch("--shorten-resource-paths",
+ "Shortens the paths of resources inside the APK.",
&options_.shorten_resource_paths);
AddOptionalFlag("--resource-path-shortening-map",
"Path to output the map of old resource paths to shortened paths.",
@@ -125,7 +123,6 @@ class OptimizeCommand : public Command {
const std::string &file_path);
Maybe<std::string> config_path_;
- Maybe<std::string> whitelist_path_;
Maybe<std::string> resources_config_path_;
Maybe<std::string> target_densities_;
std::vector<std::string> configs_;
diff --git a/tools/aapt2/cmd/Optimize_test.cpp b/tools/aapt2/cmd/Optimize_test.cpp
new file mode 100644
index 000000000000..ac681e85b3d6
--- /dev/null
+++ b/tools/aapt2/cmd/Optimize_test.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Optimize.h"
+
+#include "AppInfo.h"
+#include "Diagnostics.h"
+#include "LoadedApk.h"
+#include "Resource.h"
+#include "test/Test.h"
+
+using testing::Contains;
+using testing::Eq;
+
+namespace aapt {
+
+bool ParseConfig(const std::string&, IAaptContext*, OptimizeOptions*);
+
+using OptimizeTest = CommandTestFixture;
+
+TEST_F(OptimizeTest, ParseConfigWithNoCollapseExemptions) {
+ const std::string& content = R"(
+string/foo#no_collapse
+dimen/bar#no_collapse
+)";
+ aapt::test::Context context;
+ OptimizeOptions options;
+ ParseConfig(content, &context, &options);
+
+ const std::set<ResourceName>& name_collapse_exemptions =
+ options.table_flattener_options.name_collapse_exemptions;
+
+ ASSERT_THAT(name_collapse_exemptions.size(), Eq(2));
+ EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo")));
+ EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar")));
+}
+
+TEST_F(OptimizeTest, ParseConfigWithNoObfuscateExemptions) {
+ const std::string& content = R"(
+string/foo#no_obfuscate
+dimen/bar#no_obfuscate
+)";
+ aapt::test::Context context;
+ OptimizeOptions options;
+ ParseConfig(content, &context, &options);
+
+ const std::set<ResourceName>& name_collapse_exemptions =
+ options.table_flattener_options.name_collapse_exemptions;
+
+ ASSERT_THAT(name_collapse_exemptions.size(), Eq(2));
+ EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo")));
+ EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar")));
+}
+
+} // namespace aapt
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index b9321174100b..58e232c33985 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -228,14 +228,15 @@ class PackageFlattener {
public:
PackageFlattener(IAaptContext* context, ResourceTablePackage* package,
const std::map<size_t, std::string>* shared_libs, bool use_sparse_entries,
- bool collapse_key_stringpool, const std::set<std::string>& whitelisted_resources)
+ bool collapse_key_stringpool,
+ const std::set<ResourceName>& name_collapse_exemptions)
: context_(context),
diag_(context->GetDiagnostics()),
package_(package),
shared_libs_(shared_libs),
use_sparse_entries_(use_sparse_entries),
collapse_key_stringpool_(collapse_key_stringpool),
- whitelisted_resources_(whitelisted_resources) {
+ name_collapse_exemptions_(name_collapse_exemptions) {
}
bool FlattenPackage(BigBuffer* buffer) {
@@ -652,11 +653,12 @@ class PackageFlattener {
for (ResourceEntry* entry : sorted_entries) {
uint32_t local_key_index;
+ ResourceName resource_name({}, type->type, entry->name);
if (!collapse_key_stringpool_ ||
- whitelisted_resources_.find(entry->name) != whitelisted_resources_.end()) {
+ name_collapse_exemptions_.find(resource_name) != name_collapse_exemptions_.end()) {
local_key_index = (uint32_t)key_pool_.MakeRef(entry->name).index();
} else {
- // resource isn't whitelisted, add it as obfuscated value
+ // resource isn't exempt from collapse, add it as obfuscated value
local_key_index = (uint32_t)key_pool_.MakeRef(obfuscated_resource_name).index();
}
// Group values by configuration.
@@ -712,7 +714,7 @@ class PackageFlattener {
StringPool type_pool_;
StringPool key_pool_;
bool collapse_key_stringpool_;
- const std::set<std::string>& whitelisted_resources_;
+ const std::set<ResourceName>& name_collapse_exemptions_;
};
} // namespace
@@ -760,7 +762,7 @@ bool TableFlattener::Consume(IAaptContext* context, ResourceTable* table) {
PackageFlattener flattener(context, package.get(), &table->included_packages_,
options_.use_sparse_entries, options_.collapse_key_stringpool,
- options_.whitelisted_resources);
+ options_.name_collapse_exemptions);
if (!flattener.FlattenPackage(&package_buffer)) {
return false;
}
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index 73c17295556b..4360db190146 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -19,6 +19,7 @@
#include "android-base/macros.h"
+#include "Resource.h"
#include "ResourceTable.h"
#include "process/IResourceTableConsumer.h"
#include "util/BigBuffer.h"
@@ -41,8 +42,8 @@ struct TableFlattenerOptions {
// have name indices that point to this single value
bool collapse_key_stringpool = false;
- // Set of whitelisted resource names to avoid altering in key stringpool
- std::set<std::string> whitelisted_resources;
+ // Set of resources to avoid collapsing to a single entry in key stringpool.
+ std::set<ResourceName> name_collapse_exemptions;
// Map from original resource paths to shortened resource paths.
std::map<std::string, std::string> shortened_path_map;
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index a9409235e07a..8fbdd7f27041 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -518,7 +518,7 @@ TEST_F(TableFlattenerTest, LongSharedLibraryPackageNameIsIllegal) {
ASSERT_FALSE(Flatten(context.get(), {}, table.get(), &result));
}
-TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) {
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoNameCollapseExemptionsSucceeds) {
std::unique_ptr<ResourceTable> table =
test::ResourceTableBuilder()
.SetPackageId("com.app.test", 0x7f)
@@ -572,7 +572,7 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) {
ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u));
}
-TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) {
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithNameCollapseExemptionsSucceeds) {
std::unique_ptr<ResourceTable> table =
test::ResourceTableBuilder()
.SetPackageId("com.app.test", 0x7f)
@@ -591,21 +591,22 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) {
TableFlattenerOptions options;
options.collapse_key_stringpool = true;
- options.whitelisted_resources.insert("test");
- options.whitelisted_resources.insert("three");
+ options.name_collapse_exemptions.insert(ResourceName({}, ResourceType::kId, "one"));
+ options.name_collapse_exemptions.insert(ResourceName({}, ResourceType::kString, "test"));
ResTable res_table;
ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
- EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/one",
ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
- EXPECT_TRUE(Exists(&res_table, "com.app.test:id/three", ResourceId(0x7f020002), {},
- Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020002), {}, Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+ // Note that this resource is also named "one", but it's a different type, so gets obfuscated.
EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u,
ResTable_config::CONFIG_VERSION));
diff --git a/tools/aapt2/link/XmlReferenceLinker.cpp b/tools/aapt2/link/XmlReferenceLinker.cpp
index f3be483ab728..c3c16b92f712 100644
--- a/tools/aapt2/link/XmlReferenceLinker.cpp
+++ b/tools/aapt2/link/XmlReferenceLinker.cpp
@@ -83,6 +83,15 @@ class XmlVisitor : public xml::PackageAwareVisitor {
Attribute default_attribute(android::ResTable_map::TYPE_ANY);
default_attribute.SetWeak(true);
+ // The default orientation of gradients in android Q is different than previous android
+ // versions. Set the android:angle attribute to "0" to ensure that the default gradient
+ // orientation will remain left-to-right in android Q.
+ if (el->name == "gradient" && context_->GetMinSdkVersion() <= SDK_Q) {
+ if (!el->FindAttribute(xml::kSchemaAndroid, "angle")) {
+ el->attributes.push_back(xml::Attribute{xml::kSchemaAndroid, "angle", "0"});
+ }
+ }
+
const Source source = source_.WithLine(el->line_number);
for (xml::Attribute& attr : el->attributes) {
// If the attribute has no namespace, interpret values as if
diff --git a/tools/aapt2/link/XmlReferenceLinker_test.cpp b/tools/aapt2/link/XmlReferenceLinker_test.cpp
index ef99355e5b5f..0ce2e50d6e44 100644
--- a/tools/aapt2/link/XmlReferenceLinker_test.cpp
+++ b/tools/aapt2/link/XmlReferenceLinker_test.cpp
@@ -47,6 +47,8 @@ class XmlReferenceLinkerTest : public ::testing::Test {
test::AttributeBuilder()
.SetTypeMask(android::ResTable_map::TYPE_STRING)
.Build())
+ .AddPublicSymbol("android:attr/angle", ResourceId(0x01010004),
+ test::AttributeBuilder().Build())
// Add one real symbol that was introduces in v21
.AddPublicSymbol("android:attr/colorAccent", ResourceId(0x01010435),
@@ -75,7 +77,7 @@ class XmlReferenceLinkerTest : public ::testing::Test {
}
protected:
- std::unique_ptr<IAaptContext> context_;
+ std::unique_ptr<test::Context> context_;
};
TEST_F(XmlReferenceLinkerTest, LinkBasicAttributes) {
@@ -254,4 +256,63 @@ TEST_F(XmlReferenceLinkerTest, LinkViewWithLocalPackageAndAliasOfTheSameName) {
EXPECT_EQ(make_value(ResourceId(0x7f030000)), ref->id);
}
+
+TEST_F(XmlReferenceLinkerTest, AddAngleOnGradientForAndroidQ) {
+ std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"(
+ <gradient />)");
+
+ XmlReferenceLinker linker;
+ ASSERT_TRUE(linker.Consume(context_.get(), doc.get()));
+
+ xml::Element* gradient_el = doc->root.get();
+ ASSERT_THAT(gradient_el, NotNull());
+
+ xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle");
+ ASSERT_THAT(xml_attr, NotNull());
+ ASSERT_TRUE(xml_attr->compiled_attribute);
+ EXPECT_EQ(make_value(ResourceId(0x01010004)), xml_attr->compiled_attribute.value().id);
+
+ BinaryPrimitive* value = ValueCast<BinaryPrimitive>(xml_attr->compiled_value.get());
+ ASSERT_THAT(value, NotNull());
+ EXPECT_EQ(value->value.dataType, android::Res_value::TYPE_INT_DEC);
+ EXPECT_EQ(value->value.data, 0U);
+}
+
+TEST_F(XmlReferenceLinkerTest, DoNotOverwriteAngleOnGradientForAndroidQ) {
+ std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"(
+ <gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"/>)");
+
+ XmlReferenceLinker linker;
+ ASSERT_TRUE(linker.Consume(context_.get(), doc.get()));
+
+ xml::Element* gradient_el = doc->root.get();
+ ASSERT_THAT(gradient_el, NotNull());
+
+ xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle");
+ ASSERT_THAT(xml_attr, NotNull());
+ ASSERT_TRUE(xml_attr->compiled_attribute);
+ EXPECT_EQ(make_value(ResourceId(0x01010004)), xml_attr->compiled_attribute.value().id);
+
+ BinaryPrimitive* value = ValueCast<BinaryPrimitive>(xml_attr->compiled_value.get());
+ ASSERT_THAT(value, NotNull());
+ EXPECT_EQ(value->value.dataType, android::Res_value::TYPE_INT_DEC);
+ EXPECT_EQ(value->value.data, 90U);
+}
+
+TEST_F(XmlReferenceLinkerTest, DoNotOverwriteAngleOnGradientForPostAndroidQ) {
+ std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDomForPackageName(context_.get(), R"(
+ <gradient xmlns:android="http://schemas.android.com/apk/res/android" />)");
+ context_->SetMinSdkVersion(30);
+
+ XmlReferenceLinker linker;
+ ASSERT_TRUE(linker.Consume(context_.get(), doc.get()));
+
+ xml::Element* gradient_el = doc->root.get();
+ ASSERT_THAT(gradient_el, NotNull());
+
+ xml::Attribute* xml_attr = gradient_el->FindAttribute(xml::kSchemaAndroid, "angle");
+ ASSERT_THAT(xml_attr, IsNull());
+}
+
} // namespace aapt
diff --git a/tools/aapt2/test/Context.h b/tools/aapt2/test/Context.h
index 7e10a59df877..553c43e6c469 100644
--- a/tools/aapt2/test/Context.h
+++ b/tools/aapt2/test/Context.h
@@ -81,6 +81,10 @@ class Context : public IAaptContext {
return min_sdk_version_;
}
+ void SetMinSdkVersion(int min_sdk_version) {
+ min_sdk_version_ = min_sdk_version;
+ }
+
const std::set<std::string>& GetSplitNameDependencies() override {
return split_name_dependencies_;
}
diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt
index 5061be2091e5..92da9dab863b 100644
--- a/tools/codegen/src/com/android/codegen/ClassInfo.kt
+++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt
@@ -38,6 +38,11 @@ open class ClassInfo(val sourceLines: List<String>) {
val superInterfaces = (fileAst.types[0] as ClassOrInterfaceDeclaration)
.implementedTypes.map { it.asString() }
+ val superClass = run {
+ val superClasses = (fileAst.types[0] as ClassOrInterfaceDeclaration).extendedTypes
+ if (superClasses.isNonEmpty) superClasses[0] else null
+ }
+
val ClassName = classAst.nameAsString
private val genericArgsAst = classAst.typeParameters
val genericArgs = if (genericArgsAst.isEmpty()) "" else {
diff --git a/tools/codegen/src/com/android/codegen/ClassPrinter.kt b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
index 1f0d4b8a7ec9..bd72d9e7ec21 100644
--- a/tools/codegen/src/com/android/codegen/ClassPrinter.kt
+++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt
@@ -1,6 +1,7 @@
package com.android.codegen
import com.github.javaparser.ast.Modifier
+import com.github.javaparser.ast.body.CallableDeclaration
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
import com.github.javaparser.ast.body.TypeDeclaration
import com.github.javaparser.ast.expr.*
@@ -37,6 +38,7 @@ class ClassPrinter(
val GeneratedMember by lazy { classRef("com.android.internal.util.DataClass.Generated.Member") }
val Parcelling by lazy { classRef("com.android.internal.util.Parcelling") }
val Parcelable by lazy { classRef("android.os.Parcelable") }
+ val Parcel by lazy { classRef("android.os.Parcel") }
val UnsupportedAppUsage by lazy { classRef("android.annotation.UnsupportedAppUsage") }
init {
@@ -354,7 +356,9 @@ class ClassPrinter(
}
fun hasMethod(name: String, vararg argTypes: String): Boolean {
- return classAst.methods.any {
+ val members: List<CallableDeclaration<*>> =
+ if (name == ClassName) classAst.constructors else classAst.methods
+ return members.any {
it.name.asString() == name &&
it.parameters.map { it.type.asString() } == argTypes.toList()
}
@@ -365,6 +369,10 @@ class ClassPrinter(
.mapIndexed { i, node -> FieldInfo(index = i, fieldAst = node, classInfo = this) }
.filter { hasMethod("lazyInit${it.NameUpperCamel}") }
+ val extendsParcelableClass by lazy {
+ Parcelable !in superInterfaces && superClass != null
+ }
+
init {
val builderFactoryOverride = classAst.methods.find {
it.isStatic && it.nameAsString == "builder"
diff --git a/tools/codegen/src/com/android/codegen/Generators.kt b/tools/codegen/src/com/android/codegen/Generators.kt
index 914e475cfe41..5a95676c1dc8 100644
--- a/tools/codegen/src/com/android/codegen/Generators.kt
+++ b/tools/codegen/src/com/android/codegen/Generators.kt
@@ -341,7 +341,7 @@ private fun ClassPrinter.generateBuilderSetters(visibility: String) {
}
}
- if (Type.contains("Map<")) {
+ if (FieldClass.endsWith("Map") && FieldInnerType != null) {
generateBuilderMethod(
name = adderName,
defVisibility = visibility,
@@ -422,6 +422,10 @@ fun ClassPrinter.generateParcelable() {
+"// void parcelFieldName(Parcel dest, int flags) { ... }"
+""
+ if (extendsParcelableClass) {
+ +"super.writeToParcel(dest, flags);\n"
+ }
+
if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) {
+"$flagStorageType flg = 0;"
booleanFields.forEachApply {
@@ -463,6 +467,123 @@ fun ClassPrinter.generateParcelable() {
+""
}
+ if (!hasMethod(ClassName, Parcel)) {
+ val visibility = if (classAst.isFinal) "/* package-private */" else "protected"
+
+ +"/** @hide */"
+ +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})"
+ +GENERATED_MEMBER_HEADER
+ "$visibility $ClassName($Parcel in) {" {
+ +"// You can override field unparcelling by defining methods like:"
+ +"// static FieldType unparcelFieldName(Parcel in) { ... }"
+ +""
+
+ if (extendsParcelableClass) {
+ +"super(in);\n"
+ }
+
+ if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) {
+ +"$flagStorageType flg = in.read$FlagStorageType();"
+ }
+ booleanFields.forEachApply {
+ +"$Type $_name = (flg & $fieldBit) != 0;"
+ }
+ nonBooleanFields.forEachApply {
+
+ // Handle customized parceling
+ val customParcellingMethod = "unparcel$NameUpperCamel"
+ if (hasMethod(customParcellingMethod, Parcel)) {
+ +"$Type $_name = $customParcellingMethod(in);"
+ } else if (customParcellingClass != null) {
+ +"$Type $_name = $sParcelling.unparcel(in);"
+ } else if (hasAnnotation("@$DataClassEnum")) {
+ val ordinal = "${_name}Ordinal"
+ +"int $ordinal = in.readInt();"
+ +"$Type $_name = $ordinal < 0 ? null : $FieldClass.values()[$ordinal];"
+ } else {
+ val methodArgs = mutableListOf<String>()
+
+ // Create container if any
+ val containerInitExpr = when {
+ FieldClass.endsWith("Map") -> "new $LinkedHashMap<>()"
+ FieldClass == "List" || FieldClass == "ArrayList" ->
+ "new ${classRef("java.util.ArrayList")}<>()"
+ else -> ""
+ }
+ val passContainer = containerInitExpr.isNotEmpty()
+
+ // nullcheck +
+ // "FieldType fieldName = (FieldType)"
+ if (passContainer) {
+ methodArgs.add(_name)
+ !"$Type $_name = "
+ if (mayBeNull) {
+ +"null;"
+ !"if ((flg & $fieldBit) != 0) {"
+ pushIndent()
+ +""
+ !"$_name = "
+ }
+ +"$containerInitExpr;"
+ } else {
+ !"$Type $_name = "
+ if (mayBeNull) !"(flg & $fieldBit) == 0 ? null : "
+ if (ParcelMethodsSuffix == "StrongInterface") {
+ !"$FieldClass.Stub.asInterface("
+ } else if (Type !in PRIMITIVE_TYPES + "String" + "Bundle" &&
+ (!isArray || FieldInnerType !in PRIMITIVE_TYPES + "String") &&
+ ParcelMethodsSuffix != "Parcelable") {
+ !"($FieldClass) "
+ }
+ }
+
+ // Determine method args
+ when {
+ ParcelMethodsSuffix == "Parcelable" ->
+ methodArgs += "$FieldClass.class.getClassLoader()"
+ ParcelMethodsSuffix == "SparseArray" ->
+ methodArgs += "$FieldInnerClass.class.getClassLoader()"
+ ParcelMethodsSuffix == "TypedObject" ->
+ methodArgs += "$FieldClass.CREATOR"
+ ParcelMethodsSuffix == "TypedArray" ->
+ methodArgs += "$FieldInnerClass.CREATOR"
+ ParcelMethodsSuffix == "Map" ->
+ methodArgs += "${fieldTypeGenegicArgs[1].substringBefore("<")}.class.getClassLoader()"
+ ParcelMethodsSuffix.startsWith("Parcelable")
+ || (isList || isArray)
+ && FieldInnerType !in PRIMITIVE_TYPES + "String" ->
+ methodArgs += "$FieldInnerClass.class.getClassLoader()"
+ }
+
+ // ...in.readFieldType(args...);
+ when {
+ ParcelMethodsSuffix == "StrongInterface" -> !"in.readStrongBinder"
+ isArray -> !"in.create$ParcelMethodsSuffix"
+ else -> !"in.read$ParcelMethodsSuffix"
+ }
+ !"(${methodArgs.joinToString(", ")})"
+ if (ParcelMethodsSuffix == "StrongInterface") !")"
+ +";"
+
+ // Cleanup if passContainer
+ if (passContainer && mayBeNull) {
+ popIndent()
+ rmEmptyLine()
+ +"\n}"
+ }
+ }
+ }
+
+ +""
+ fields.forEachApply {
+ !"this."
+ generateSetFrom(_name)
+ }
+
+ generateOnConstructedCallback()
+ }
+ }
+
if (classAst.fields.none { it.variables[0].nameAsString == "CREATOR" }) {
val Creator = classRef("android.os.Parcelable.Creator")
@@ -477,104 +598,8 @@ fun ClassPrinter.generateParcelable() {
}
+"@Override"
- +"@SuppressWarnings({\"unchecked\", \"RedundantCast\"})"
"public $ClassName createFromParcel($Parcel in)" {
- +"// You can override field unparcelling by defining methods like:"
- +"// static FieldType unparcelFieldName(Parcel in) { ... }"
- +""
- if (booleanFields.isNotEmpty() || nullableFields.isNotEmpty()) {
- +"$flagStorageType flg = in.read$FlagStorageType();"
- }
- booleanFields.forEachApply {
- +"$Type $_name = (flg & $fieldBit) != 0;"
- }
- nonBooleanFields.forEachApply {
-
- // Handle customized parceling
- val customParcellingMethod = "unparcel$NameUpperCamel"
- if (hasMethod(customParcellingMethod, Parcel)) {
- +"$Type $_name = $customParcellingMethod(in);"
- } else if (customParcellingClass != null) {
- +"$Type $_name = $sParcelling.unparcel(in);"
- } else if (hasAnnotation("@$DataClassEnum")) {
- val ordinal = "${_name}Ordinal"
- +"int $ordinal = in.readInt();"
- +"$Type $_name = $ordinal < 0 ? null : $FieldClass.values()[$ordinal];"
- } else {
- val methodArgs = mutableListOf<String>()
-
- // Create container if any
- val containerInitExpr = when {
- FieldClass.endsWith("Map") -> "new $LinkedHashMap<>()"
- FieldClass == "List" || FieldClass == "ArrayList" ->
- "new ${classRef("java.util.ArrayList")}<>()"
- else -> ""
- }
- val passContainer = containerInitExpr.isNotEmpty()
-
- // nullcheck +
- // "FieldType fieldName = (FieldType)"
- if (passContainer) {
- methodArgs.add(_name)
- !"$Type $_name = "
- if (mayBeNull) {
- +"null;"
- !"if ((flg & $fieldBit) != 0) {"
- pushIndent()
- +""
- !"$_name = "
- }
- +"$containerInitExpr;"
- } else {
- !"$Type $_name = "
- if (mayBeNull) !"(flg & $fieldBit) == 0 ? null : "
- if (ParcelMethodsSuffix == "StrongInterface") {
- !"$FieldClass.Stub.asInterface("
- } else if (Type !in PRIMITIVE_TYPES + "String" + "Bundle" &&
- (!isArray || FieldInnerType !in PRIMITIVE_TYPES + "String") &&
- ParcelMethodsSuffix != "Parcelable") {
- !"($Type) "
- }
- }
-
- // Determine method args
- when {
- ParcelMethodsSuffix == "Parcelable" ->
- methodArgs += "$FieldClass.class.getClassLoader()"
- ParcelMethodsSuffix == "TypedObject" ->
- methodArgs += "$FieldClass.CREATOR"
- ParcelMethodsSuffix == "TypedArray" ->
- methodArgs += "$FieldInnerClass.CREATOR"
- ParcelMethodsSuffix.startsWith("Parcelable")
- || FieldClass == "Map"
- || (isList || isArray)
- && FieldInnerType !in PRIMITIVE_TYPES + "String" ->
- methodArgs += "$FieldInnerClass.class.getClassLoader()"
- }
-
- // ...in.readFieldType(args...);
- when {
- ParcelMethodsSuffix == "StrongInterface" -> !"in.readStrongBinder"
- isArray -> !"in.create$ParcelMethodsSuffix"
- else -> !"in.read$ParcelMethodsSuffix"
- }
- !"(${methodArgs.joinToString(", ")})"
- if (ParcelMethodsSuffix == "StrongInterface") !")"
- +";"
-
- // Cleanup if passContainer
- if (passContainer && mayBeNull) {
- popIndent()
- rmEmptyLine()
- +"\n}"
- }
- }
- }
- "return new $ClassType(" {
- fields.forEachTrimmingTrailingComma {
- +"$_name,"
- }
- } + ";"
+ +"return new $ClassName(in);"
}
rmEmptyLine()
} + ";"
diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt
index 0f932f3c34e1..039f7b2fc627 100755
--- a/tools/codegen/src/com/android/codegen/Main.kt
+++ b/tools/codegen/src/com/android/codegen/Main.kt
@@ -95,7 +95,13 @@ In addition, for any field mMyField(or myField) of type FieldType you can define
you can use with final fields.
Version: $CODEGEN_VERSION
-Questions? Feedback? Contact: eugenesusla@
+
+Questions? Feedback?
+Contact: eugenesusla@
+Bug/feature request: http://go/codegen-bug
+
+Slides: http://go/android-codegen
+In-depth example: http://go/SampleDataClass
"""
fun main(args: Array<String>) {
@@ -107,7 +113,7 @@ fun main(args: Array<String>) {
println(CODEGEN_VERSION)
System.exit(0)
}
- val file = File(args.last())
+ val file = File(args.last()).absoluteFile
val sourceLinesNoClosingBrace = file.readLines().dropLastWhile {
it.startsWith("}") || it.all(Char::isWhitespace)
}
@@ -132,11 +138,11 @@ fun main(args: Array<String>) {
// $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION.
//
// DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
//
// To regenerate run:
// $ $cliExecutable ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped
- //
- // CHECKSTYLE:OFF Generated code
+
"""
if (FeatureFlag.CONST_DEFS()) generateConstDefs()
@@ -146,8 +152,7 @@ fun main(args: Array<String>) {
generateConstructor("public")
} else if (FeatureFlag.BUILDER()
|| FeatureFlag.COPY_CONSTRUCTOR()
- || FeatureFlag.WITHERS()
- || FeatureFlag.PARCELABLE()) {
+ || FeatureFlag.WITHERS()) {
generateConstructor("/* package-private */")
}
if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor()
diff --git a/tools/codegen/src/com/android/codegen/SharedConstants.kt b/tools/codegen/src/com/android/codegen/SharedConstants.kt
index 7d50ad10de00..a36f2c838787 100644
--- a/tools/codegen/src/com/android/codegen/SharedConstants.kt
+++ b/tools/codegen/src/com/android/codegen/SharedConstants.kt
@@ -1,7 +1,7 @@
package com.android.codegen
const val CODEGEN_NAME = "codegen"
-const val CODEGEN_VERSION = "1.0.0"
+const val CODEGEN_VERSION = "1.0.5"
const val CANONICAL_BUILDER_CLASS = "Builder"
const val BASE_BUILDER_CLASS = "BaseBuilder"
diff --git a/tools/protologtool/Android.bp b/tools/protologtool/Android.bp
new file mode 100644
index 000000000000..d1a86c245dec
--- /dev/null
+++ b/tools/protologtool/Android.bp
@@ -0,0 +1,33 @@
+java_library_host {
+ name: "protologtool-lib",
+ srcs: [
+ "src/com/android/protolog/tool/**/*.kt",
+ ],
+ static_libs: [
+ "protolog-common",
+ "javaparser",
+ "protolog-proto",
+ "jsonlib",
+ ],
+}
+
+java_binary_host {
+ name: "protologtool",
+ manifest: "manifest.txt",
+ static_libs: [
+ "protologtool-lib",
+ ],
+}
+
+java_test_host {
+ name: "protologtool-tests",
+ test_suites: ["general-tests"],
+ srcs: [
+ "tests/**/*.kt",
+ ],
+ static_libs: [
+ "protologtool-lib",
+ "junit",
+ "mockito",
+ ],
+}
diff --git a/tools/protologtool/README.md b/tools/protologtool/README.md
new file mode 100644
index 000000000000..ba639570f88c
--- /dev/null
+++ b/tools/protologtool/README.md
@@ -0,0 +1,119 @@
+# ProtoLogTool
+
+Code transformation tool and viewer for ProtoLog.
+
+## What does it do?
+
+ProtoLogTool incorporates three different modes of operation:
+
+### Code transformation
+
+Command: `protologtool transform-protolog-calls
+ --protolog-class <protolog class name>
+ --protolog-impl-class <protolog implementation class name>
+ --loggroups-class <protolog groups class name>
+ --loggroups-jar <config jar path>
+ --output-srcjar <output.srcjar>
+ [<input.java>]`
+
+In this mode ProtoLogTool transforms every ProtoLog logging call in form of:
+```java
+ProtoLog.x(ProtoLogGroup.GROUP_NAME, "Format string %d %s", value1, value2);
+```
+into:
+```java
+if (ProtoLogImpl.isEnabled(GROUP_NAME)) {
+ int protoLogParam0 = value1;
+ String protoLogParam1 = String.valueOf(value2);
+ ProtoLogImpl.x(ProtoLogGroup.GROUP_NAME, 123456, 0b0100, "Format string %d %s or null", protoLogParam0, protoLogParam1);
+}
+```
+where `ProtoLog`, `ProtoLogImpl` and `ProtoLogGroup` are the classes provided as arguments
+ (can be imported, static imported or full path, wildcard imports are not allowed) and, `x` is the
+ logging method. The transformation is done on the source level. A hash is generated from the format
+ string, log level and log group name and inserted after the `ProtoLogGroup` argument. After the hash
+ we insert a bitmask specifying the types of logged parameters. The format string is replaced
+ by `null` if `ProtoLogGroup.GROUP_NAME.isLogToLogcat()` returns false. If `ProtoLogGroup.GROUP_NAME.isEnabled()`
+ returns false the log statement is removed entirely from the resultant code. The real generated code is inlined
+ and a number of new line characters is added as to preserve line numbering in file.
+
+Input is provided as a list of java source file names. Transformed source is saved to a single
+source jar file. The ProtoLogGroup class with all dependencies should be provided as a compiled
+jar file (config.jar).
+
+### Viewer config generation
+
+Command: `generate-viewer-config
+ --protolog-class <protolog class name>
+ --loggroups-class <protolog groups class name>
+ --loggroups-jar <config jar path>
+ --viewer-conf <viewer.json>
+ [<input.java>]`
+
+This command is similar in it's syntax to the previous one, only instead of creating a processed source jar
+it writes a viewer configuration file with following schema:
+```json
+{
+ "version": "1.0.0",
+ "messages": {
+ "123456": {
+ "message": "Format string %d %s",
+ "level": "ERROR",
+ "group": "GROUP_NAME",
+ "at": "com\/android\/server\/example\/Class.java"
+ }
+ },
+ "groups": {
+ "GROUP_NAME": {
+ "tag": "TestLog"
+ }
+ }
+}
+
+```
+
+### Binary log viewing
+
+Command: `read-log --viewer-conf <viewer.json> <wm_log.pb>`
+
+Reads the binary ProtoLog log file and outputs a human-readable LogCat-like text log.
+
+## What is ProtoLog?
+
+ProtoLog is a generic logging system created for the WindowManager project. It allows both binary and text logging
+and is tunable in runtime. It consists of 3 different submodules:
+* logging system built-in the Android app,
+* log viewer for reading binary logs,
+* a code processing tool.
+
+ProtoLog is designed to reduce both application size (and by that memory usage) and amount of resources needed
+for logging. This is achieved by replacing log message strings with their hashes and only loading to memory/writing
+full log messages when necessary.
+
+### Text logging
+
+For text-based logs Android LogCat is used as a backend. Message strings are loaded from a viewer config
+located on the device when needed.
+
+### Binary logging
+
+Binary logs are saved as Protocol Buffers file. They can be read using the ProtoLog tool or specialised
+viewer like Winscope.
+
+## How to use ProtoLog?
+
+### Adding a new logging group or log statement
+
+To add a new ProtoLogGroup simple create a new enum ProtoLogGroup member with desired parameters.
+
+To add a new logging statement just add a new call to ProtoLog.x where x is a log level.
+
+After doing any changes to logging groups or statements you should build the project and follow instructions printed by the tool.
+
+## How to change settings on device in runtime?
+Use the `adb shell su root cmd window logging` command. To get help just type
+`adb shell su root cmd window logging help`.
+
+
+
+
diff --git a/tools/protologtool/TEST_MAPPING b/tools/protologtool/TEST_MAPPING
new file mode 100644
index 000000000000..52b12dc26be9
--- /dev/null
+++ b/tools/protologtool/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "protologtool-tests"
+ }
+ ]
+}
diff --git a/tools/protologtool/manifest.txt b/tools/protologtool/manifest.txt
new file mode 100644
index 000000000000..cabebd51a2fa
--- /dev/null
+++ b/tools/protologtool/manifest.txt
@@ -0,0 +1 @@
+Main-class: com.android.protolog.tool.ProtoLogTool
diff --git a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
new file mode 100644
index 000000000000..a52c8042582b
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.ImportDeclaration
+import com.github.javaparser.ast.expr.BinaryExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.StringLiteralExpr
+
+object CodeUtils {
+ /**
+ * Returns a stable hash of a string.
+ * We reimplement String::hashCode() for readability reasons.
+ */
+ fun hash(position: String, messageString: String, logLevel: LogLevel, logGroup: LogGroup): Int {
+ return (position + messageString + logLevel.name + logGroup.name)
+ .map { c -> c.toInt() }.reduce { h, c -> h * 31 + c }
+ }
+
+ fun checkWildcardStaticImported(code: CompilationUnit, className: String, fileName: String) {
+ code.findAll(ImportDeclaration::class.java)
+ .forEach { im ->
+ if (im.isStatic && im.isAsterisk && im.name.toString() == className) {
+ throw IllegalImportException("Wildcard static imports of $className " +
+ "methods are not supported.", ParsingContext(fileName, im))
+ }
+ }
+ }
+
+ fun isClassImportedOrSamePackage(code: CompilationUnit, className: String): Boolean {
+ val packageName = className.substringBeforeLast('.')
+ return code.packageDeclaration.isPresent &&
+ code.packageDeclaration.get().nameAsString == packageName ||
+ code.findAll(ImportDeclaration::class.java)
+ .any { im ->
+ !im.isStatic &&
+ ((!im.isAsterisk && im.name.toString() == className) ||
+ (im.isAsterisk && im.name.toString() == packageName))
+ }
+ }
+
+ fun staticallyImportedMethods(code: CompilationUnit, className: String): Set<String> {
+ return code.findAll(ImportDeclaration::class.java)
+ .filter { im ->
+ im.isStatic &&
+ im.name.toString().substringBeforeLast('.') == className
+ }
+ .map { im -> im.name.toString().substringAfterLast('.') }.toSet()
+ }
+
+ fun concatMultilineString(expr: Expression, context: ParsingContext): String {
+ return when (expr) {
+ is StringLiteralExpr -> expr.asString()
+ is BinaryExpr -> when {
+ expr.operator == BinaryExpr.Operator.PLUS ->
+ concatMultilineString(expr.left, context) +
+ concatMultilineString(expr.right, context)
+ else -> throw InvalidProtoLogCallException(
+ "expected a string literal " +
+ "or concatenation of string literals, got: $expr", context)
+ }
+ else -> throw InvalidProtoLogCallException("expected a string literal " +
+ "or concatenation of string literals, got: $expr", context)
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
new file mode 100644
index 000000000000..3dfa4d216cc2
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import java.util.regex.Pattern
+
+class CommandOptions(args: Array<String>) {
+ companion object {
+ const val TRANSFORM_CALLS_CMD = "transform-protolog-calls"
+ const val GENERATE_CONFIG_CMD = "generate-viewer-config"
+ const val READ_LOG_CMD = "read-log"
+ private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD)
+
+ private const val PROTOLOG_CLASS_PARAM = "--protolog-class"
+ private const val PROTOLOGIMPL_CLASS_PARAM = "--protolog-impl-class"
+ private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class"
+ private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar"
+ private const val VIEWER_CONFIG_JSON_PARAM = "--viewer-conf"
+ private const val OUTPUT_SOURCE_JAR_PARAM = "--output-srcjar"
+ private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGIMPL_CLASS_PARAM,
+ PROTOLOGGROUP_CLASS_PARAM, PROTOLOGGROUP_JAR_PARAM, VIEWER_CONFIG_JSON_PARAM,
+ OUTPUT_SOURCE_JAR_PARAM)
+
+ val USAGE = """
+ Usage: ${Constants.NAME} <command> [<args>]
+ Available commands:
+
+ $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGIMPL_CLASS_PARAM
+ <class name> $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM
+ <config.jar> $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>]
+ - processes java files replacing stub calls with logging code.
+
+ $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGGROUP_CLASS_PARAM
+ <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar> $VIEWER_CONFIG_JSON_PARAM
+ <viewer.json> [<input.java>]
+ - creates viewer config file from given java files.
+
+ $READ_LOG_CMD $VIEWER_CONFIG_JSON_PARAM <viewer.json> <wm_log.pb>
+ - translates a binary log to a readable format.
+ """.trimIndent()
+
+ private fun validateClassName(name: String): String {
+ if (!Pattern.matches("^([a-z]+[A-Za-z0-9]*\\.)+([A-Za-z0-9]+)$", name)) {
+ throw InvalidCommandException("Invalid class name $name")
+ }
+ return name
+ }
+
+ private fun getParam(paramName: String, params: Map<String, String>): String {
+ if (!params.containsKey(paramName)) {
+ throw InvalidCommandException("Param $paramName required")
+ }
+ return params.getValue(paramName)
+ }
+
+ private fun validateNotSpecified(paramName: String, params: Map<String, String>): String {
+ if (params.containsKey(paramName)) {
+ throw InvalidCommandException("Unsupported param $paramName")
+ }
+ return ""
+ }
+
+ private fun validateJarName(name: String): String {
+ if (!name.endsWith(".jar")) {
+ throw InvalidCommandException("Jar file required, got $name instead")
+ }
+ return name
+ }
+
+ private fun validateSrcJarName(name: String): String {
+ if (!name.endsWith(".srcjar")) {
+ throw InvalidCommandException("Source jar file required, got $name instead")
+ }
+ return name
+ }
+
+ private fun validateJSONName(name: String): String {
+ if (!name.endsWith(".json")) {
+ throw InvalidCommandException("Json file required, got $name instead")
+ }
+ return name
+ }
+
+ private fun validateJavaInputList(list: List<String>): List<String> {
+ if (list.isEmpty()) {
+ throw InvalidCommandException("No java source input files")
+ }
+ list.forEach { name ->
+ if (!name.endsWith(".java")) {
+ throw InvalidCommandException("Not a java source file $name")
+ }
+ }
+ return list
+ }
+
+ private fun validateLogInputList(list: List<String>): String {
+ if (list.isEmpty()) {
+ throw InvalidCommandException("No log input file")
+ }
+ if (list.size > 1) {
+ throw InvalidCommandException("Only one log input file allowed")
+ }
+ return list[0]
+ }
+ }
+
+ val protoLogClassNameArg: String
+ val protoLogGroupsClassNameArg: String
+ val protoLogImplClassNameArg: String
+ val protoLogGroupsJarArg: String
+ val viewerConfigJsonArg: String
+ val outputSourceJarArg: String
+ val logProtofileArg: String
+ val javaSourceArgs: List<String>
+ val command: String
+
+ init {
+ if (args.isEmpty()) {
+ throw InvalidCommandException("No command specified.")
+ }
+ command = args[0]
+ if (command !in commands) {
+ throw InvalidCommandException("Unknown command.")
+ }
+
+ val params: MutableMap<String, String> = mutableMapOf()
+ val inputFiles: MutableList<String> = mutableListOf()
+
+ var idx = 1
+ while (idx < args.size) {
+ if (args[idx].startsWith("--")) {
+ if (idx + 1 >= args.size) {
+ throw InvalidCommandException("No value for ${args[idx]}")
+ }
+ if (args[idx] !in parameters) {
+ throw InvalidCommandException("Unknown parameter ${args[idx]}")
+ }
+ if (args[idx + 1].startsWith("--")) {
+ throw InvalidCommandException("No value for ${args[idx]}")
+ }
+ if (params.containsKey(args[idx])) {
+ throw InvalidCommandException("Duplicated parameter ${args[idx]}")
+ }
+ params[args[idx]] = args[idx + 1]
+ idx += 2
+ } else {
+ inputFiles.add(args[idx])
+ idx += 1
+ }
+ }
+
+ when (command) {
+ TRANSFORM_CALLS_CMD -> {
+ protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
+ protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
+ params))
+ protoLogImplClassNameArg = validateClassName(getParam(PROTOLOGIMPL_CLASS_PARAM,
+ params))
+ protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
+ viewerConfigJsonArg = validateNotSpecified(VIEWER_CONFIG_JSON_PARAM, params)
+ outputSourceJarArg = validateSrcJarName(getParam(OUTPUT_SOURCE_JAR_PARAM, params))
+ javaSourceArgs = validateJavaInputList(inputFiles)
+ logProtofileArg = ""
+ }
+ GENERATE_CONFIG_CMD -> {
+ protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
+ protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
+ params))
+ protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
+ protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
+ viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+ outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+ javaSourceArgs = validateJavaInputList(inputFiles)
+ logProtofileArg = ""
+ }
+ READ_LOG_CMD -> {
+ protoLogClassNameArg = validateNotSpecified(PROTOLOG_CLASS_PARAM, params)
+ protoLogGroupsClassNameArg = validateNotSpecified(PROTOLOGGROUP_CLASS_PARAM, params)
+ protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
+ protoLogGroupsJarArg = validateNotSpecified(PROTOLOGGROUP_JAR_PARAM, params)
+ viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+ outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+ javaSourceArgs = listOf()
+ logProtofileArg = validateLogInputList(inputFiles)
+ }
+ else -> {
+ throw InvalidCommandException("Unknown command.")
+ }
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/Constants.kt b/tools/protologtool/src/com/android/protolog/tool/Constants.kt
new file mode 100644
index 000000000000..aa3e00f2f4db
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/Constants.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+object Constants {
+ const val NAME = "protologtool"
+ const val VERSION = "1.0.0"
+ const val IS_ENABLED_METHOD = "isEnabled"
+ const val ENUM_VALUES_METHOD = "values"
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt b/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt
new file mode 100644
index 000000000000..587f7b9db016
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/LogGroup.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+data class LogGroup(
+ val name: String,
+ val enabled: Boolean,
+ val textEnabled: Boolean,
+ val tag: String
+)
diff --git a/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt b/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt
new file mode 100644
index 000000000000..e88f0f8231bd
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/LogLevel.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.ast.Node
+
+enum class LogLevel {
+ DEBUG, VERBOSE, INFO, WARN, ERROR, WTF;
+
+ companion object {
+ fun getLevelForMethodName(name: String, node: Node, context: ParsingContext): LogLevel {
+ return when (name) {
+ "d" -> DEBUG
+ "v" -> VERBOSE
+ "i" -> INFO
+ "w" -> WARN
+ "e" -> ERROR
+ "wtf" -> WTF
+ else ->
+ throw InvalidProtoLogCallException("Unknown log level $name in $node", context)
+ }
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/LogParser.kt b/tools/protologtool/src/com/android/protolog/tool/LogParser.kt
new file mode 100644
index 000000000000..a59038fc99a0
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/LogParser.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.json.stream.JsonReader
+import com.android.server.protolog.common.InvalidFormatStringException
+import com.android.server.protolog.common.LogDataType
+import com.android.server.protolog.ProtoLogMessage
+import com.android.server.protolog.ProtoLogFileProto
+import java.io.BufferedReader
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.io.PrintStream
+import java.lang.Exception
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Implements a simple parser/viewer for binary ProtoLog logs.
+ * A binary log is translated into Android "LogCat"-like text log.
+ */
+class LogParser(private val configParser: ViewerConfigParser) {
+ companion object {
+ private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+ private val magicNumber =
+ ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+ ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+ }
+
+ private fun printTime(time: Long, offset: Long, ps: PrintStream) {
+ ps.print(dateFormat.format(Date(time / 1000000 + offset)) + " ")
+ }
+
+ private fun printFormatted(
+ protoLogMessage: ProtoLogMessage,
+ configEntry: ViewerConfigParser.ConfigEntry,
+ ps: PrintStream
+ ) {
+ val strParmIt = protoLogMessage.strParamsList.iterator()
+ val longParamsIt = protoLogMessage.sint64ParamsList.iterator()
+ val doubleParamsIt = protoLogMessage.doubleParamsList.iterator()
+ val boolParamsIt = protoLogMessage.booleanParamsList.iterator()
+ val args = mutableListOf<Any>()
+ val format = configEntry.messageString
+ val argTypes = LogDataType.parseFormatString(format)
+ try {
+ argTypes.forEach {
+ when (it) {
+ LogDataType.BOOLEAN -> args.add(boolParamsIt.next())
+ LogDataType.LONG -> args.add(longParamsIt.next())
+ LogDataType.DOUBLE -> args.add(doubleParamsIt.next())
+ LogDataType.STRING -> args.add(strParmIt.next())
+ null -> throw NullPointerException()
+ }
+ }
+ } catch (ex: NoSuchElementException) {
+ throw InvalidFormatStringException("Invalid format string in config", ex)
+ }
+ if (strParmIt.hasNext() || longParamsIt.hasNext() ||
+ doubleParamsIt.hasNext() || boolParamsIt.hasNext()) {
+ throw RuntimeException("Invalid format string in config - no enough matchers")
+ }
+ val formatted = format.format(*(args.toTypedArray()))
+ ps.print("${configEntry.level} ${configEntry.tag}: $formatted\n")
+ }
+
+ private fun printUnformatted(protoLogMessage: ProtoLogMessage, ps: PrintStream, tag: String) {
+ ps.println("$tag: ${protoLogMessage.messageHash} - ${protoLogMessage.strParamsList}" +
+ " ${protoLogMessage.sint64ParamsList} ${protoLogMessage.doubleParamsList}" +
+ " ${protoLogMessage.booleanParamsList}")
+ }
+
+ fun parse(protoLogInput: InputStream, jsonConfigInput: InputStream, ps: PrintStream) {
+ val jsonReader = JsonReader(BufferedReader(InputStreamReader(jsonConfigInput)))
+ val config = configParser.parseConfig(jsonReader)
+ val protoLog = ProtoLogFileProto.parseFrom(protoLogInput)
+
+ if (protoLog.magicNumber != magicNumber) {
+ throw InvalidInputException("ProtoLog file magic number is invalid.")
+ }
+ if (protoLog.version != Constants.VERSION) {
+ throw InvalidInputException("ProtoLog file version not supported by this tool," +
+ " log version ${protoLog.version}, viewer version ${Constants.VERSION}")
+ }
+
+ protoLog.logList.forEach { log ->
+ printTime(log.elapsedRealtimeNanos, protoLog.realTimeToElapsedTimeOffsetMillis, ps)
+ if (log.messageHash !in config) {
+ printUnformatted(log, ps, "UNKNOWN")
+ } else {
+ val conf = config.getValue(log.messageHash)
+ try {
+ printFormatted(log, conf, ps)
+ } catch (ex: Exception) {
+ printUnformatted(log, ps, "INVALID")
+ }
+ }
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt b/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt
new file mode 100644
index 000000000000..c6aedfc3093e
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ParsingContext.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.ast.Node
+
+data class ParsingContext(val filePath: String, val lineNumber: Int) {
+ constructor(filePath: String, node: Node)
+ : this(filePath, if (node.range.isPresent) node.range.get().begin.line else -1)
+
+ constructor() : this("", -1)
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
new file mode 100644
index 000000000000..2181cf680f6c
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.FieldAccessExpr
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
+
+/**
+ * Helper class for visiting all ProtoLog calls.
+ * For every valid call in the given {@code CompilationUnit} a {@code ProtoLogCallVisitor} callback
+ * is executed.
+ */
+open class ProtoLogCallProcessor(
+ private val protoLogClassName: String,
+ private val protoLogGroupClassName: String,
+ private val groupMap: Map<String, LogGroup>
+) {
+ private val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.')
+ private val protoLogGroupSimpleClassName = protoLogGroupClassName.substringAfterLast('.')
+
+ private fun getLogGroupName(
+ expr: Expression,
+ isClassImported: Boolean,
+ staticImports: Set<String>,
+ fileName: String
+ ): String {
+ val context = ParsingContext(fileName, expr)
+ return when (expr) {
+ is NameExpr -> when {
+ expr.nameAsString in staticImports -> expr.nameAsString
+ else ->
+ throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr",
+ context)
+ }
+ is FieldAccessExpr -> when {
+ expr.scope.toString() == protoLogGroupClassName
+ || isClassImported &&
+ expr.scope.toString() == protoLogGroupSimpleClassName -> expr.nameAsString
+ else ->
+ throw InvalidProtoLogCallException("Unknown/not imported ProtoLogGroup: $expr",
+ context)
+ }
+ else -> throw InvalidProtoLogCallException("Invalid group argument " +
+ "- must be ProtoLogGroup enum member reference: $expr", context)
+ }
+ }
+
+ private fun isProtoCall(
+ call: MethodCallExpr,
+ isLogClassImported: Boolean,
+ staticLogImports: Collection<String>
+ ): Boolean {
+ return call.scope.isPresent && call.scope.get().toString() == protoLogClassName ||
+ isLogClassImported && call.scope.isPresent &&
+ call.scope.get().toString() == protoLogSimpleClassName ||
+ !call.scope.isPresent && staticLogImports.contains(call.name.toString())
+ }
+
+ open fun process(code: CompilationUnit, callVisitor: ProtoLogCallVisitor?, fileName: String):
+ CompilationUnit {
+ CodeUtils.checkWildcardStaticImported(code, protoLogClassName, fileName)
+ CodeUtils.checkWildcardStaticImported(code, protoLogGroupClassName, fileName)
+
+ val isLogClassImported = CodeUtils.isClassImportedOrSamePackage(code, protoLogClassName)
+ val staticLogImports = CodeUtils.staticallyImportedMethods(code, protoLogClassName)
+ val isGroupClassImported = CodeUtils.isClassImportedOrSamePackage(code,
+ protoLogGroupClassName)
+ val staticGroupImports = CodeUtils.staticallyImportedMethods(code, protoLogGroupClassName)
+
+ code.findAll(MethodCallExpr::class.java)
+ .filter { call ->
+ isProtoCall(call, isLogClassImported, staticLogImports)
+ }.forEach { call ->
+ val context = ParsingContext(fileName, call)
+ if (call.arguments.size < 2) {
+ throw InvalidProtoLogCallException("Method signature does not match " +
+ "any ProtoLog method: $call", context)
+ }
+
+ val messageString = CodeUtils.concatMultilineString(call.getArgument(1),
+ context)
+ val groupNameArg = call.getArgument(0)
+ val groupName =
+ getLogGroupName(groupNameArg, isGroupClassImported,
+ staticGroupImports, fileName)
+ if (groupName !in groupMap) {
+ throw InvalidProtoLogCallException("Unknown group argument " +
+ "- not a ProtoLogGroup enum member: $call", context)
+ }
+
+ callVisitor?.processCall(call, messageString, LogLevel.getLevelForMethodName(
+ call.name.toString(), call, context), groupMap.getValue(groupName))
+ }
+ return code
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt
new file mode 100644
index 000000000000..aa58b69d61cb
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallVisitor.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.ast.expr.MethodCallExpr
+
+interface ProtoLogCallVisitor {
+ fun processCall(call: MethodCallExpr, messageString: String, level: LogLevel, group: LogGroup)
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt
new file mode 100644
index 000000000000..75493b6427cb
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogGroupReader.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.protolog.tool.Constants.ENUM_VALUES_METHOD
+import com.android.server.protolog.common.IProtoLogGroup
+import java.io.File
+import java.net.URLClassLoader
+
+class ProtoLogGroupReader {
+ private fun getClassloaderForJar(jarPath: String): ClassLoader {
+ val jarFile = File(jarPath)
+ val url = jarFile.toURI().toURL()
+ return URLClassLoader(arrayOf(url), ProtoLogGroupReader::class.java.classLoader)
+ }
+
+ private fun getEnumValues(clazz: Class<*>): List<IProtoLogGroup> {
+ val valuesMethod = clazz.getMethod(ENUM_VALUES_METHOD)
+ @Suppress("UNCHECKED_CAST")
+ return (valuesMethod.invoke(null) as Array<IProtoLogGroup>).toList()
+ }
+
+ fun loadFromJar(jarPath: String, className: String): Map<String, LogGroup> {
+ try {
+ val classLoader = getClassloaderForJar(jarPath)
+ val clazz = classLoader.loadClass(className)
+ val values = getEnumValues(clazz)
+ return values.map { group ->
+ group.name() to
+ LogGroup(group.name(), group.isEnabled, group.isLogToLogcat, group.tag)
+ }.toMap()
+ } catch (ex: ReflectiveOperationException) {
+ throw RuntimeException("Unable to load ProtoLogGroup enum class", ex)
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
new file mode 100644
index 000000000000..70ac0bee59b3
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.protolog.tool.CommandOptions.Companion.USAGE
+import com.github.javaparser.ParseProblemException
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.CompilationUnit
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.util.jar.JarOutputStream
+import java.util.zip.ZipEntry
+import kotlin.system.exitProcess
+
+object ProtoLogTool {
+ private fun showHelpAndExit() {
+ println(USAGE)
+ exitProcess(-1)
+ }
+
+ private fun containsProtoLogText(source: String, protoLogClassName: String): Boolean {
+ val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.')
+ return source.contains(protoLogSimpleClassName)
+ }
+
+ private fun processClasses(command: CommandOptions) {
+ val groups = ProtoLogGroupReader()
+ .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg)
+ val out = FileOutputStream(command.outputSourceJarArg)
+ val outJar = JarOutputStream(out)
+ val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
+ command.protoLogGroupsClassNameArg, groups)
+ val transformer = SourceTransformer(command.protoLogImplClassNameArg, processor)
+
+ command.javaSourceArgs.forEach { path ->
+ val file = File(path)
+ val text = file.readText()
+ val newPath = path
+ val outSrc = try {
+ val code = tryParse(text, path)
+ if (containsProtoLogText(text, command.protoLogClassNameArg)) {
+ transformer.processClass(text, newPath, code)
+ } else {
+ text
+ }
+ } catch (ex: ParsingException) {
+ // If we cannot parse this file, skip it (and log why). Compilation will fail
+ // in a subsequent build step.
+ println("\n${ex.message}\n")
+ text
+ }
+ outJar.putNextEntry(ZipEntry(newPath))
+ outJar.write(outSrc.toByteArray())
+ outJar.closeEntry()
+ }
+
+ outJar.close()
+ out.close()
+ }
+
+ private fun tryParse(code: String, fileName: String): CompilationUnit {
+ try {
+ return StaticJavaParser.parse(code)
+ } catch (ex: ParseProblemException) {
+ val problem = ex.problems.first()
+ throw ParsingException("Java parsing erro" +
+ "r: ${problem.verboseMessage}",
+ ParsingContext(fileName, problem.location.orElse(null)
+ ?.begin?.range?.orElse(null)?.begin?.line
+ ?: 0))
+ }
+ }
+
+ private fun viewerConf(command: CommandOptions) {
+ val groups = ProtoLogGroupReader()
+ .loadFromJar(command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg)
+ val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
+ command.protoLogGroupsClassNameArg, groups)
+ val builder = ViewerConfigBuilder(processor)
+ command.javaSourceArgs.forEach { path ->
+ val file = File(path)
+ val text = file.readText()
+ if (containsProtoLogText(text, command.protoLogClassNameArg)) {
+ try {
+ val code = tryParse(text, path)
+ val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration
+ .get().nameAsString else ""
+ val newPath = pack.replace('.', '/') + '/' + file.name
+ builder.processClass(code, newPath)
+ } catch (ex: ParsingException) {
+ // If we cannot parse this file, skip it (and log why). Compilation will fail
+ // in a subsequent build step.
+ println("\n${ex.message}\n")
+ }
+ }
+ }
+ val out = FileOutputStream(command.viewerConfigJsonArg)
+ out.write(builder.build().toByteArray())
+ out.close()
+ }
+
+ private fun read(command: CommandOptions) {
+ LogParser(ViewerConfigParser())
+ .parse(FileInputStream(command.logProtofileArg),
+ FileInputStream(command.viewerConfigJsonArg), System.out)
+ }
+
+ @JvmStatic
+ fun main(args: Array<String>) {
+ try {
+ val command = CommandOptions(args)
+ when (command.command) {
+ CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command)
+ CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command)
+ CommandOptions.READ_LOG_CMD -> read(command)
+ }
+ } catch (ex: InvalidCommandException) {
+ println("\n${ex.message}\n")
+ showHelpAndExit()
+ } catch (ex: CodeProcessingException) {
+ println("\n${ex.message}\n")
+ exitProcess(1)
+ }
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
new file mode 100644
index 000000000000..00fd038079f9
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.protolog.tool.Constants.IS_ENABLED_METHOD
+import com.android.server.protolog.common.LogDataType
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.NodeList
+import com.github.javaparser.ast.body.VariableDeclarator
+import com.github.javaparser.ast.expr.BooleanLiteralExpr
+import com.github.javaparser.ast.expr.CastExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.FieldAccessExpr
+import com.github.javaparser.ast.expr.IntegerLiteralExpr
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
+import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.SimpleName
+import com.github.javaparser.ast.expr.TypeExpr
+import com.github.javaparser.ast.expr.VariableDeclarationExpr
+import com.github.javaparser.ast.stmt.BlockStmt
+import com.github.javaparser.ast.stmt.ExpressionStmt
+import com.github.javaparser.ast.stmt.IfStmt
+import com.github.javaparser.ast.type.ArrayType
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+import com.github.javaparser.ast.type.PrimitiveType
+import com.github.javaparser.ast.type.Type
+import com.github.javaparser.printer.PrettyPrinter
+import com.github.javaparser.printer.PrettyPrinterConfiguration
+import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter
+
+class SourceTransformer(
+ protoLogImplClassName: String,
+ private val protoLogCallProcessor: ProtoLogCallProcessor
+) : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ // Input format: ProtoLog.e(GROUP, "msg %d", arg)
+ if (!call.parentNode.isPresent) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- no parent node in AST")
+ }
+ if (call.parentNode.get() !is ExpressionStmt) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- parent node in AST is not an ExpressionStmt")
+ }
+ val parentStmt = call.parentNode.get() as ExpressionStmt
+ if (!parentStmt.parentNode.isPresent) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- no grandparent node in AST")
+ }
+ val ifStmt: IfStmt
+ if (group.enabled) {
+ val hash = CodeUtils.hash(fileName, messageString, level, group)
+ val newCall = call.clone()
+ if (!group.textEnabled) {
+ // Remove message string if text logging is not enabled by default.
+ // Out: ProtoLog.e(GROUP, null, arg)
+ newCall.arguments[1].replace(NameExpr("null"))
+ }
+ // Insert message string hash as a second argument.
+ // Out: ProtoLog.e(GROUP, 1234, null, arg)
+ newCall.arguments.add(1, IntegerLiteralExpr(hash))
+ val argTypes = LogDataType.parseFormatString(messageString)
+ val typeMask = LogDataType.logDataTypesToBitMask(argTypes)
+ // Insert bitmap representing which Number parameters are to be considered as
+ // floating point numbers.
+ // Out: ProtoLog.e(GROUP, 1234, 0, null, arg)
+ newCall.arguments.add(2, IntegerLiteralExpr(typeMask))
+ // Replace call to a stub method with an actual implementation.
+ // Out: com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, null, arg)
+ newCall.setScope(protoLogImplClassNode)
+ // Create a call to ProtoLogImpl.isEnabled(GROUP)
+ // Out: com.android.server.protolog.ProtoLogImpl.isEnabled(GROUP)
+ val isLogEnabled = MethodCallExpr(protoLogImplClassNode, IS_ENABLED_METHOD,
+ NodeList<Expression>(newCall.arguments[0].clone()))
+ if (argTypes.size != call.arguments.size - 2) {
+ throw InvalidProtoLogCallException(
+ "Number of arguments (${argTypes.size} does not mach format" +
+ " string in: $call", ParsingContext(fileName, call))
+ }
+ val blockStmt = BlockStmt()
+ if (argTypes.isNotEmpty()) {
+ // Assign every argument to a variable to check its type in compile time
+ // (this is assignment is optimized-out by dex tool, there is no runtime impact)/
+ // Out: long protoLogParam0 = arg
+ argTypes.forEachIndexed { idx, type ->
+ val varName = "protoLogParam$idx"
+ val declaration = VariableDeclarator(getASTTypeForDataType(type), varName,
+ getConversionForType(type)(newCall.arguments[idx + 4].clone()))
+ blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration)))
+ newCall.setArgument(idx + 4, NameExpr(SimpleName(varName)))
+ }
+ } else {
+ // Assign (Object[])null as the vararg parameter to prevent allocating an empty
+ // object array.
+ val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr())
+ newCall.addArgument(nullArray)
+ }
+ blockStmt.addStatement(ExpressionStmt(newCall))
+ // Create an IF-statement with the previously created condition.
+ // Out: if (com.android.server.protolog.ProtoLogImpl.isEnabled(GROUP)) {
+ // long protoLogParam0 = arg;
+ // com.android.server.protolog.ProtoLogImpl.e(GROUP, 1234, 0, null, protoLogParam0);
+ // }
+ ifStmt = IfStmt(isLogEnabled, blockStmt, null)
+ } else {
+ // Surround with if (false).
+ val newCall = parentStmt.clone()
+ ifStmt = IfStmt(BooleanLiteralExpr(false), BlockStmt(NodeList(newCall)), null)
+ newCall.setBlockComment(" ${group.name} is disabled ")
+ }
+ // Inline the new statement.
+ val printedIfStmt = inlinePrinter.print(ifStmt)
+ // Append blank lines to preserve line numbering in file (to allow debugging)
+ val newLines = LexicalPreservingPrinter.print(parentStmt).count { c -> c == '\n' }
+ val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}'
+ // pre-workaround code, see explanation below
+ /*
+ val inlinedIfStmt = StaticJavaParser.parseStatement(newStmt)
+ LexicalPreservingPrinter.setup(inlinedIfStmt)
+ // Replace the original call.
+ if (!parentStmt.replace(inlinedIfStmt)) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- unable to replace the call.")
+ }
+ */
+ /** Workaround for a bug in JavaParser (AST tree invalid after replacing a node when using
+ * LexicalPreservingPrinter (https://github.com/javaparser/javaparser/issues/2290).
+ * Replace the code below with the one commended-out above one the issue is resolved. */
+ if (!parentStmt.range.isPresent) {
+ // Should never happen
+ throw RuntimeException("Unable to process log call $call " +
+ "- unable to replace the call.")
+ }
+ val range = parentStmt.range.get()
+ val begin = range.begin.line - 1
+ val oldLines = processedCode.subList(begin, range.end.line)
+ val oldCode = oldLines.joinToString("\n")
+ val newCode = oldCode.replaceRange(
+ offsets[begin] + range.begin.column - 1,
+ oldCode.length - oldLines.lastOrNull()!!.length +
+ range.end.column + offsets[range.end.line - 1], newStmt)
+ newCode.split("\n").forEachIndexed { idx, line ->
+ offsets[begin + idx] += line.length - processedCode[begin + idx].length
+ processedCode[begin + idx] = line
+ }
+ }
+
+ private val inlinePrinter: PrettyPrinter
+ private val objectType = StaticJavaParser.parseClassOrInterfaceType("Object")
+
+ init {
+ val config = PrettyPrinterConfiguration()
+ config.endOfLineCharacter = " "
+ config.indentSize = 0
+ config.tabWidth = 1
+ inlinePrinter = PrettyPrinter(config)
+ }
+
+ companion object {
+ private val stringType: ClassOrInterfaceType =
+ StaticJavaParser.parseClassOrInterfaceType("String")
+
+ fun getASTTypeForDataType(type: Int): Type {
+ return when (type) {
+ LogDataType.STRING -> stringType.clone()
+ LogDataType.LONG -> PrimitiveType.longType()
+ LogDataType.DOUBLE -> PrimitiveType.doubleType()
+ LogDataType.BOOLEAN -> PrimitiveType.booleanType()
+ else -> {
+ // Should never happen.
+ throw RuntimeException("Invalid LogDataType")
+ }
+ }
+ }
+
+ fun getConversionForType(type: Int): (Expression) -> Expression {
+ return when (type) {
+ LogDataType.STRING -> { expr ->
+ MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")),
+ SimpleName("valueOf"), NodeList(expr))
+ }
+ else -> { expr -> expr }
+ }
+ }
+ }
+
+ private val protoLogImplClassNode =
+ StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName)
+ private var processedCode: MutableList<String> = mutableListOf()
+ private var offsets: IntArray = IntArray(0)
+ private var fileName: String = ""
+
+ fun processClass(
+ code: String,
+ path: String,
+ compilationUnit: CompilationUnit =
+ StaticJavaParser.parse(code)
+ ): String {
+ fileName = path
+ processedCode = code.split('\n').toMutableList()
+ offsets = IntArray(processedCode.size)
+ LexicalPreservingPrinter.setup(compilationUnit)
+ protoLogCallProcessor.process(compilationUnit, this, fileName)
+ // return LexicalPreservingPrinter.print(compilationUnit)
+ return processedCode.joinToString("\n")
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt
new file mode 100644
index 000000000000..941455a24ec8
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.json.stream.JsonWriter
+import com.github.javaparser.ast.CompilationUnit
+import com.android.protolog.tool.Constants.VERSION
+import com.github.javaparser.ast.expr.MethodCallExpr
+import java.io.StringWriter
+
+class ViewerConfigBuilder(
+ private val protoLogCallVisitor: ProtoLogCallProcessor
+) : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ if (group.enabled) {
+ val position = fileName
+ val key = CodeUtils.hash(position, messageString, level, group)
+ if (statements.containsKey(key)) {
+ if (statements[key] != LogCall(messageString, level, group, position)) {
+ throw HashCollisionException(
+ "Please modify the log message \"$messageString\" " +
+ "or \"${statements[key]}\" - their hashes are equal.",
+ ParsingContext(fileName, call))
+ }
+ } else {
+ groups.add(group)
+ statements[key] = LogCall(messageString, level, group, position)
+ call.range.isPresent
+ }
+ }
+ }
+
+ private val statements: MutableMap<Int, LogCall> = mutableMapOf()
+ private val groups: MutableSet<LogGroup> = mutableSetOf()
+ private var fileName: String = ""
+
+ fun processClass(unit: CompilationUnit, fileName: String) {
+ this.fileName = fileName
+ protoLogCallVisitor.process(unit, this, fileName)
+ }
+
+ fun build(): String {
+ val stringWriter = StringWriter()
+ val writer = JsonWriter(stringWriter)
+ writer.setIndent(" ")
+ writer.beginObject()
+ writer.name("version")
+ writer.value(VERSION)
+ writer.name("messages")
+ writer.beginObject()
+ statements.toSortedMap().forEach { (key, value) ->
+ writer.name(key.toString())
+ writer.beginObject()
+ writer.name("message")
+ writer.value(value.messageString)
+ writer.name("level")
+ writer.value(value.logLevel.name)
+ writer.name("group")
+ writer.value(value.logGroup.name)
+ writer.name("at")
+ writer.value(value.position)
+ writer.endObject()
+ }
+ writer.endObject()
+ writer.name("groups")
+ writer.beginObject()
+ groups.toSortedSet(Comparator { o1, o2 -> o1.name.compareTo(o2.name) }).forEach { group ->
+ writer.name(group.name)
+ writer.beginObject()
+ writer.name("tag")
+ writer.value(group.tag)
+ writer.endObject()
+ }
+ writer.endObject()
+ writer.endObject()
+ stringWriter.buffer.append('\n')
+ return stringWriter.toString()
+ }
+
+ data class LogCall(
+ val messageString: String,
+ val logLevel: LogLevel,
+ val logGroup: LogGroup,
+ val position: String
+ )
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
new file mode 100644
index 000000000000..7278db0094e6
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.json.stream.JsonReader
+
+open class ViewerConfigParser {
+ data class MessageEntry(
+ val messageString: String,
+ val level: String,
+ val groupName: String
+ )
+
+ fun parseMessage(jsonReader: JsonReader): MessageEntry {
+ jsonReader.beginObject()
+ var message: String? = null
+ var level: String? = null
+ var groupName: String? = null
+ while (jsonReader.hasNext()) {
+ when (jsonReader.nextName()) {
+ "message" -> message = jsonReader.nextString()
+ "level" -> level = jsonReader.nextString()
+ "group" -> groupName = jsonReader.nextString()
+ else -> jsonReader.skipValue()
+ }
+ }
+ jsonReader.endObject()
+ if (message.isNullOrBlank() || level.isNullOrBlank() || groupName.isNullOrBlank()) {
+ throw InvalidViewerConfigException("Invalid message entry in viewer config")
+ }
+ return MessageEntry(message, level, groupName)
+ }
+
+ data class GroupEntry(val tag: String)
+
+ fun parseGroup(jsonReader: JsonReader): GroupEntry {
+ jsonReader.beginObject()
+ var tag: String? = null
+ while (jsonReader.hasNext()) {
+ when (jsonReader.nextName()) {
+ "tag" -> tag = jsonReader.nextString()
+ else -> jsonReader.skipValue()
+ }
+ }
+ jsonReader.endObject()
+ if (tag.isNullOrBlank()) {
+ throw InvalidViewerConfigException("Invalid group entry in viewer config")
+ }
+ return GroupEntry(tag)
+ }
+
+ fun parseMessages(jsonReader: JsonReader): Map<Int, MessageEntry> {
+ val config: MutableMap<Int, MessageEntry> = mutableMapOf()
+ jsonReader.beginObject()
+ while (jsonReader.hasNext()) {
+ val key = jsonReader.nextName()
+ val hash = key.toIntOrNull()
+ ?: throw InvalidViewerConfigException("Invalid key in messages viewer config")
+ config[hash] = parseMessage(jsonReader)
+ }
+ jsonReader.endObject()
+ return config
+ }
+
+ fun parseGroups(jsonReader: JsonReader): Map<String, GroupEntry> {
+ val config: MutableMap<String, GroupEntry> = mutableMapOf()
+ jsonReader.beginObject()
+ while (jsonReader.hasNext()) {
+ val key = jsonReader.nextName()
+ config[key] = parseGroup(jsonReader)
+ }
+ jsonReader.endObject()
+ return config
+ }
+
+ data class ConfigEntry(val messageString: String, val level: String, val tag: String)
+
+ open fun parseConfig(jsonReader: JsonReader): Map<Int, ConfigEntry> {
+ var messages: Map<Int, MessageEntry>? = null
+ var groups: Map<String, GroupEntry>? = null
+ var version: String? = null
+
+ jsonReader.beginObject()
+ while (jsonReader.hasNext()) {
+ when (jsonReader.nextName()) {
+ "messages" -> messages = parseMessages(jsonReader)
+ "groups" -> groups = parseGroups(jsonReader)
+ "version" -> version = jsonReader.nextString()
+
+ else -> jsonReader.skipValue()
+ }
+ }
+ jsonReader.endObject()
+ if (messages == null || groups == null || version == null) {
+ throw InvalidViewerConfigException("Invalid config - definitions missing")
+ }
+ if (version != Constants.VERSION) {
+ throw InvalidViewerConfigException("Viewer config version not supported by this tool," +
+ " config version $version, viewer version ${Constants.VERSION}")
+ }
+ return messages.map { msg ->
+ msg.key to ConfigEntry(
+ msg.value.messageString, msg.value.level, groups[msg.value.groupName]?.tag
+ ?: throw InvalidViewerConfigException(
+ "Group definition missing for ${msg.value.groupName}"))
+ }.toMap()
+ }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/exceptions.kt b/tools/protologtool/src/com/android/protolog/tool/exceptions.kt
new file mode 100644
index 000000000000..ae00df123353
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/exceptions.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import java.lang.Exception
+
+open class CodeProcessingException(message: String, context: ParsingContext)
+ : Exception("Code processing error in ${context.filePath}:${context.lineNumber}:\n" +
+ " $message")
+
+class HashCollisionException(message: String, context: ParsingContext) :
+ CodeProcessingException(message, context)
+
+class IllegalImportException(message: String, context: ParsingContext) :
+ CodeProcessingException("Illegal import: $message", context)
+
+class InvalidProtoLogCallException(message: String, context: ParsingContext)
+ : CodeProcessingException("InvalidProtoLogCall: $message", context)
+
+class ParsingException(message: String, context: ParsingContext)
+ : CodeProcessingException(message, context)
+
+class InvalidViewerConfigException(message: String) : Exception(message)
+
+class InvalidInputException(message: String) : Exception(message)
+
+class InvalidCommandException(message: String) : Exception(message)
diff --git a/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
new file mode 100644
index 000000000000..b916f8f00a68
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.expr.BinaryExpr
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class CodeUtilsTest {
+ @Test
+ fun hash() {
+ assertEquals(-1259556708, CodeUtils.hash("Test.java:50", "test",
+ LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
+ }
+
+ @Test
+ fun hash_changeLocation() {
+ assertEquals(15793504, CodeUtils.hash("Test.java:10", "test2",
+ LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
+ }
+
+ @Test
+ fun hash_changeLevel() {
+ assertEquals(-731772463, CodeUtils.hash("Test.java:50", "test",
+ LogLevel.ERROR, LogGroup("test", true, true, "TAG")))
+ }
+
+ @Test
+ fun hash_changeMessage() {
+ assertEquals(-2026343204, CodeUtils.hash("Test.java:50", "test2",
+ LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
+ }
+
+ @Test
+ fun hash_changeGroup() {
+ assertEquals(1607870166, CodeUtils.hash("Test.java:50", "test2",
+ LogLevel.DEBUG, LogGroup("test2", true, true, "TAG")))
+ }
+
+ @Test(expected = IllegalImportException::class)
+ fun checkWildcardStaticImported_true() {
+ val code = """package org.example.test;
+ import static org.example.Test.*;
+ """
+ CodeUtils.checkWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test", "")
+ }
+
+ @Test
+ fun checkWildcardStaticImported_notStatic() {
+ val code = """package org.example.test;
+ import org.example.Test.*;
+ """
+ CodeUtils.checkWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test", "")
+ }
+
+ @Test
+ fun checkWildcardStaticImported_differentClass() {
+ val code = """package org.example.test;
+ import static org.example.Test2.*;
+ """
+ CodeUtils.checkWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test", "")
+ }
+
+ @Test
+ fun checkWildcardStaticImported_notWildcard() {
+ val code = """package org.example.test;
+ import org.example.Test.test;
+ """
+ CodeUtils.checkWildcardStaticImported(
+ StaticJavaParser.parse(code), "org.example.Test", "")
+ }
+
+ @Test
+ fun isClassImportedOrSamePackage_imported() {
+ val code = """package org.example.test;
+ import org.example.Test;
+ """
+ assertTrue(CodeUtils.isClassImportedOrSamePackage(
+ StaticJavaParser.parse(code), "org.example.Test"))
+ }
+
+ @Test
+ fun isClassImportedOrSamePackage_samePackage() {
+ val code = """package org.example.test;
+ """
+ assertTrue(CodeUtils.isClassImportedOrSamePackage(
+ StaticJavaParser.parse(code), "org.example.test.Test"))
+ }
+
+ @Test
+ fun isClassImportedOrSamePackage_false() {
+ val code = """package org.example.test;
+ import org.example.Test;
+ """
+ assertFalse(CodeUtils.isClassImportedOrSamePackage(
+ StaticJavaParser.parse(code), "org.example.Test2"))
+ }
+
+ @Test
+ fun staticallyImportedMethods_ab() {
+ val code = """
+ import static org.example.Test.a;
+ import static org.example.Test.b;
+ """
+ val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code),
+ "org.example.Test")
+ assertTrue(imported.containsAll(listOf("a", "b")))
+ assertEquals(2, imported.size)
+ }
+
+ @Test
+ fun staticallyImportedMethods_differentClass() {
+ val code = """
+ import static org.example.Test.a;
+ import static org.example.Test2.b;
+ """
+ val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code),
+ "org.example.Test")
+ assertTrue(imported.containsAll(listOf("a")))
+ assertEquals(1, imported.size)
+ }
+
+ @Test
+ fun staticallyImportedMethods_notStatic() {
+ val code = """
+ import static org.example.Test.a;
+ import org.example.Test.b;
+ """
+ val imported = CodeUtils.staticallyImportedMethods(StaticJavaParser.parse(code),
+ "org.example.Test")
+ assertTrue(imported.containsAll(listOf("a")))
+ assertEquals(1, imported.size)
+ }
+
+ @Test
+ fun concatMultilineString_single() {
+ val str = StringLiteralExpr("test")
+ val out = CodeUtils.concatMultilineString(str, ParsingContext())
+ assertEquals("test", out)
+ }
+
+ @Test
+ fun concatMultilineString_double() {
+ val str = """
+ "test" + "abc"
+ """
+ val code = StaticJavaParser.parseExpression<BinaryExpr>(str)
+ val out = CodeUtils.concatMultilineString(code, ParsingContext())
+ assertEquals("testabc", out)
+ }
+
+ @Test
+ fun concatMultilineString_multiple() {
+ val str = """
+ "test" + "abc" + "1234" + "test"
+ """
+ val code = StaticJavaParser.parseExpression<BinaryExpr>(str)
+ val out = CodeUtils.concatMultilineString(code, ParsingContext())
+ assertEquals("testabc1234test", out)
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
new file mode 100644
index 000000000000..615712e10bcf
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class CommandOptionsTest {
+ companion object {
+ val TEST_JAVA_SRC = listOf(
+ "frameworks/base/services/core/java/com/android/server/wm/" +
+ "AccessibilityController.java",
+ "frameworks/base/services/core/java/com/android/server/wm/ActivityDisplay.java",
+ "frameworks/base/services/core/java/com/android/server/wm/" +
+ "ActivityMetricsLaunchObserver.java"
+ )
+ private const val TEST_PROTOLOG_CLASS = "com.android.server.wm.ProtoLog"
+ private const val TEST_PROTOLOGIMPL_CLASS = "com.android.server.wm.ProtoLogImpl"
+ private const val TEST_PROTOLOGGROUP_CLASS = "com.android.server.wm.ProtoLogGroup"
+ private const val TEST_PROTOLOGGROUP_JAR = "out/soong/.intermediates/frameworks/base/" +
+ "services/core/services.core.wm.protologgroups/android_common/javac/" +
+ "services.core.wm.protologgroups.jar"
+ private const val TEST_SRC_JAR = "out/soong/.temp/sbox175955373/" +
+ "services.core.wm.protolog.srcjar"
+ private const val TEST_VIEWER_JSON = "out/soong/.temp/sbox175955373/" +
+ "services.core.wm.protolog.json"
+ private const val TEST_LOG = "./test_log.pb"
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun noCommand() {
+ CommandOptions(arrayOf())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun invalidCommand() {
+ val testLine = "invalid"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test
+ fun transformClasses() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
+ assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+ assertEquals(TEST_PROTOLOGIMPL_CLASS, cmd.protoLogImplClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
+ assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogClass() {
+ val testLine = "transform-protolog-calls " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogImplClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogGroupClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noProtoLogGroupJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noOutJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ TEST_JAVA_SRC.joinToString(" ")
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noJavaInput() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogClass() {
+ val testLine = "transform-protolog-calls --protolog-class invalid " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogImplClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class invalid " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogGroupClass() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class invalid " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidProtoLogGroupJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar invalid.txt " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidOutJar() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar invalid.db ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_invalidJavaInput() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR invalid.py"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_unknownParam() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--unknown test --protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun transformClasses_noValue() {
+ val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--protolog-impl-class " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test
+ fun generateConfig() {
+ val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-conf $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command)
+ assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+ assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+ assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+ assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun generateConfig_noViewerConfig() {
+ val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ TEST_JAVA_SRC.joinToString(" ")
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test(expected = InvalidCommandException::class)
+ fun generateConfig_invalidViewerConfig() {
+ val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+ "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+ "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+ "--viewer-conf invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}"
+ CommandOptions(testLine.split(' ').toTypedArray())
+ }
+
+ @Test
+ fun readLog() {
+ val testLine = "read-log --viewer-conf $TEST_VIEWER_JSON $TEST_LOG"
+ val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+ assertEquals(CommandOptions.READ_LOG_CMD, cmd.command)
+ assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+ assertEquals(TEST_LOG, cmd.logProtofileArg)
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
new file mode 100644
index 000000000000..04a3bfa499d8
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.json.stream.JsonReader
+import com.android.server.protolog.ProtoLogMessage
+import com.android.server.protolog.ProtoLogFileProto
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PrintStream
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class LogParserTest {
+ private val configParser: ViewerConfigParser = mock(ViewerConfigParser::class.java)
+ private val parser = LogParser(configParser)
+ private var config: MutableMap<Int, ViewerConfigParser.ConfigEntry> = mutableMapOf()
+ private var outStream: OutputStream = ByteArrayOutputStream()
+ private var printStream: PrintStream = PrintStream(outStream)
+ private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+
+ @Before
+ fun init() {
+ Mockito.`when`(configParser.parseConfig(any(JsonReader::class.java))).thenReturn(config)
+ }
+
+ private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+ private fun getConfigDummyStream(): InputStream {
+ return "".byteInputStream()
+ }
+
+ private fun buildProtoInput(logBuilder: ProtoLogFileProto.Builder): InputStream {
+ logBuilder.setVersion(Constants.VERSION)
+ logBuilder.magicNumber =
+ ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+ ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+ return logBuilder.build().toByteArray().inputStream()
+ }
+
+ private fun testDate(timeMS: Long): String {
+ return dateFormat.format(Date(timeMS))
+ }
+
+ @Test
+ fun parse() {
+ config[70933285] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b",
+ "ERROR", "WindowManager")
+
+ val logBuilder = ProtoLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(70933285)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: true\n",
+ outStream.toString())
+ }
+
+ @Test
+ fun parse_formatting() {
+ config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" +
+ " %x %e %g %s %f", "ERROR", "WindowManager")
+
+ val logBuilder = ProtoLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(123)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ .addAllSint64Params(listOf(1000, 20000, 300000))
+ .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1))
+ .addStrParams("test")
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} ERROR WindowManager: Test completed successfully: " +
+ "true 1000 % 47040 493e0 1.000000e-01 1.00000e-05 test 1000.100000\n",
+ outStream.toString())
+ }
+
+ @Test
+ fun parse_invalidParamsTooMany() {
+ config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o",
+ "ERROR", "WindowManager")
+
+ val logBuilder = ProtoLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(123)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ .addAllSint64Params(listOf(1000, 20000, 300000))
+ .addAllDoubleParams(listOf(0.1, 0.00001, 1000.1))
+ .addStrParams("test")
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} INVALID: 123 - [test] [1000, 20000, 300000] " +
+ "[0.1, 1.0E-5, 1000.1] [true]\n", outStream.toString())
+ }
+
+ @Test
+ fun parse_invalidParamsNotEnough() {
+ config[123] = ViewerConfigParser.ConfigEntry("Test completed successfully: %b %d %% %o" +
+ " %x %e %g %s %f", "ERROR", "WindowManager")
+
+ val logBuilder = ProtoLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(123)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ .addStrParams("test")
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} INVALID: 123 - [test] [] [] [true]\n",
+ outStream.toString())
+ }
+
+ @Test(expected = InvalidInputException::class)
+ fun parse_invalidMagicNumber() {
+ val logBuilder = ProtoLogFileProto.newBuilder()
+ logBuilder.setVersion(Constants.VERSION)
+ logBuilder.magicNumber = 0
+ val stream = logBuilder.build().toByteArray().inputStream()
+
+ parser.parse(stream, getConfigDummyStream(), printStream)
+ }
+
+ @Test(expected = InvalidInputException::class)
+ fun parse_invalidVersion() {
+ val logBuilder = ProtoLogFileProto.newBuilder()
+ logBuilder.setVersion("invalid")
+ logBuilder.magicNumber =
+ ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or
+ ProtoLogFileProto.MagicNumber.MAGIC_NUMBER_L.number.toLong()
+ val stream = logBuilder.build().toByteArray().inputStream()
+
+ parser.parse(stream, getConfigDummyStream(), printStream)
+ }
+
+ @Test
+ fun parse_noConfig() {
+ val logBuilder = ProtoLogFileProto.newBuilder()
+ val logMessageBuilder = ProtoLogMessage.newBuilder()
+ logMessageBuilder
+ .setMessageHash(70933285)
+ .setElapsedRealtimeNanos(0)
+ .addBooleanParams(true)
+ logBuilder.addLog(logMessageBuilder.build())
+
+ parser.parse(buildProtoInput(logBuilder), getConfigDummyStream(), printStream)
+
+ assertEquals("${testDate(0)} UNKNOWN: 70933285 - [] [] [] [true]\n",
+ outStream.toString())
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt
new file mode 100644
index 000000000000..97f67a0a3fdb
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.expr.MethodCallExpr
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ProtoLogCallProcessorTest {
+ private data class LogCall(
+ val call: MethodCallExpr,
+ val messageString: String,
+ val level: LogLevel,
+ val group: LogGroup
+ )
+
+ private val groupMap: MutableMap<String, LogGroup> = mutableMapOf()
+ private val calls: MutableList<LogCall> = mutableListOf()
+ private val visitor = ProtoLogCallProcessor("org.example.ProtoLog", "org.example.ProtoLogGroup",
+ groupMap)
+ private val processor = object : ProtoLogCallVisitor {
+ override fun processCall(
+ call: MethodCallExpr,
+ messageString: String,
+ level: LogLevel,
+ group: LogGroup
+ ) {
+ calls.add(LogCall(call, messageString, level, group))
+ }
+ }
+
+ private fun checkCalls() {
+ assertEquals(1, calls.size)
+ val c = calls[0]
+ assertEquals("test %b", c.messageString)
+ assertEquals(groupMap["TEST"], c.group)
+ assertEquals(LogLevel.DEBUG, c.level)
+ }
+
+ @Test
+ fun process_samePackage() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ ProtoLog.e(ProtoLogGroup.ERROR, "error %d", 1);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, false, "WindowManager")
+ groupMap["ERROR"] = LogGroup("ERROR", true, true, "WindowManagerERROR")
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ assertEquals(2, calls.size)
+ var c = calls[0]
+ assertEquals("test %b", c.messageString)
+ assertEquals(groupMap["TEST"], c.group)
+ assertEquals(LogLevel.DEBUG, c.level)
+ c = calls[1]
+ assertEquals("error %d", c.messageString)
+ assertEquals(groupMap["ERROR"], c.group)
+ assertEquals(LogLevel.ERROR, c.level)
+ }
+
+ @Test
+ fun process_imported() {
+ val code = """
+ package org.example2;
+
+ import org.example.ProtoLog;
+ import org.example.ProtoLogGroup;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ checkCalls()
+ }
+
+ @Test
+ fun process_importedStatic() {
+ val code = """
+ package org.example2;
+
+ import static org.example.ProtoLog.d;
+ import static org.example.ProtoLogGroup.TEST;
+
+ class Test {
+ void test() {
+ d(TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ checkCalls()
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_groupNotImported() {
+ val code = """
+ package org.example2;
+
+ import org.example.ProtoLog;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ }
+
+ @Test
+ fun process_protoLogNotImported() {
+ val code = """
+ package org.example2;
+
+ import org.example.ProtoLogGroup;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", true, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ assertEquals(0, calls.size)
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_unknownGroup() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_staticGroup() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(TEST, "test %b", true);
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_badGroup() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(0, "test %b", true);
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ }
+
+ @Test(expected = InvalidProtoLogCallException::class)
+ fun process_invalidSignature() {
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d("test");
+ }
+ }
+ """
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ }
+
+ @Test
+ fun process_disabled() {
+ // Disabled groups are also processed.
+ val code = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.d(ProtoLogGroup.TEST, "test %b", true);
+ }
+ }
+ """
+ groupMap["TEST"] = LogGroup("TEST", false, true, "WindowManager")
+ visitor.process(StaticJavaParser.parse(code), processor, "")
+ checkCalls()
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
new file mode 100644
index 000000000000..e746300646c8
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.stmt.IfStmt
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+import org.mockito.Mockito
+
+class SourceTransformerTest {
+ companion object {
+ private const val PROTO_LOG_IMPL_PATH = "org.example.ProtoLogImpl"
+
+ /* ktlint-disable max-line-length */
+ private val TEST_CODE = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1);
+ }
+ }
+ """.trimIndent()
+
+ private val TEST_CODE_MULTILINE = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.w(TEST_GROUP, "test %d %f " +
+ "abc %s\n test", 100,
+ 0.1, "test");
+ }
+ }
+ """.trimIndent()
+
+ private val TEST_CODE_MULTICALLS = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1);
+ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1);
+ }
+ }
+ """.trimIndent()
+
+ private val TEST_CODE_NO_PARAMS = """
+ package org.example;
+
+ class Test {
+ void test() {
+ ProtoLog.w(TEST_GROUP, "test");
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_TEXT_ENABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2);
+
+ }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_NO_PARAMS = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { org.example.ProtoLogImpl.w(TEST_GROUP, -1741986185, 0, "test", (Object[]) null); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_TEXT_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, null, protoLogParam0, protoLogParam1); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (org.example.ProtoLogImpl.isEnabled(TEST_GROUP)) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, null, protoLogParam0, protoLogParam1, protoLogParam2);
+
+ }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); }
+ }
+ }
+ """.trimIndent()
+
+ private val TRANSFORMED_CODE_MULTILINE_DISABLED = """
+ package org.example;
+
+ class Test {
+ void test() {
+ if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f " + "abc %s\n test", 100, 0.1, "test");
+
+ }
+ }
+ }
+ """.trimIndent()
+ /* ktlint-enable max-line-length */
+
+ private const val PATH = "com.example.Test.java"
+ }
+
+ private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java)
+ private val implPath = "org.example.ProtoLogImpl"
+ private val sourceJarWriter = SourceTransformer(implPath, processor)
+
+ private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+ @Test
+ fun processClass_textEnabled() {
+ var code = StaticJavaParser.parse(TEST_CODE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(3, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(6, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("1698911065", methodCall.arguments[1].toString())
+ assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
+ assertEquals("\"test %d %f\"", methodCall.arguments[3].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals(TRANSFORMED_CODE_TEXT_ENABLED, out)
+ }
+
+ @Test
+ fun processClass_textEnabledMulticalls() {
+ var code = StaticJavaParser.parse(TEST_CODE_MULTICALLS)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ val calls = code.findAll(MethodCallExpr::class.java)
+ visitor.processCall(calls[0], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"))
+ visitor.processCall(calls[1], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"))
+ visitor.processCall(calls[2], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE_MULTICALLS, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(3, ifStmts.size)
+ val ifStmt = ifStmts[1]
+ assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(3, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(6, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("1698911065", methodCall.arguments[1].toString())
+ assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
+ assertEquals("\"test %d %f\"", methodCall.arguments[3].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED, out)
+ }
+
+ @Test
+ fun processClass_textEnabledMultiline() {
+ var code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
+ "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
+ true, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(4, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(7, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("1780316587", methodCall.arguments[1].toString())
+ assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals("protoLogParam2", methodCall.arguments[6].toString())
+ assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED, out)
+ }
+
+ @Test
+ fun processClass_noParams() {
+ var code = StaticJavaParser.parse(TEST_CODE_NO_PARAMS)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE_NO_PARAMS, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(1, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(5, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("-1741986185", methodCall.arguments[1].toString())
+ assertEquals(0.toString(), methodCall.arguments[2].toString())
+ assertEquals(TRANSFORMED_CODE_NO_PARAMS, out)
+ }
+
+ @Test
+ fun processClass_textDisabled() {
+ var code = StaticJavaParser.parse(TEST_CODE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", true, false, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(3, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(6, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("1698911065", methodCall.arguments[1].toString())
+ assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
+ assertEquals("null", methodCall.arguments[3].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals(TRANSFORMED_CODE_TEXT_DISABLED, out)
+ }
+
+ @Test
+ fun processClass_textDisabledMultiline() {
+ var code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
+ "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
+ true, false, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("$implPath.${Constants.IS_ENABLED_METHOD}(TEST_GROUP)",
+ ifStmt.condition.toString())
+ assertFalse(ifStmt.elseStmt.isPresent)
+ assertEquals(4, ifStmt.thenStmt.childNodes.size)
+ val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
+ assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+ assertEquals("w", methodCall.name.asString())
+ assertEquals(7, methodCall.arguments.size)
+ assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
+ assertEquals("1780316587", methodCall.arguments[1].toString())
+ assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
+ assertEquals("null", methodCall.arguments[3].toString())
+ assertEquals("protoLogParam0", methodCall.arguments[4].toString())
+ assertEquals("protoLogParam1", methodCall.arguments[5].toString())
+ assertEquals("protoLogParam2", methodCall.arguments[6].toString())
+ assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out)
+ }
+
+ @Test
+ fun processClass_disabled() {
+ var code = StaticJavaParser.parse(TEST_CODE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
+ LogLevel.WARN, LogGroup("TEST_GROUP", false, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("false", ifStmt.condition.toString())
+ assertEquals(TRANSFORMED_CODE_DISABLED, out)
+ }
+
+ @Test
+ fun processClass_disabledMultiline() {
+ var code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
+
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
+ "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
+ false, true, "WM_TEST"))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, code)
+ code = StaticJavaParser.parse(out)
+
+ val ifStmts = code.findAll(IfStmt::class.java)
+ assertEquals(1, ifStmts.size)
+ val ifStmt = ifStmts[0]
+ assertEquals("false", ifStmt.condition.toString())
+ assertEquals(TRANSFORMED_CODE_MULTILINE_DISABLED, out)
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt
new file mode 100644
index 000000000000..2b6abcdee7ed
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.json.stream.JsonReader
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.expr.MethodCallExpr
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+import java.io.StringReader
+
+class ViewerConfigBuilderTest {
+ companion object {
+ private val TAG1 = "WM_TEST"
+ private val TAG2 = "WM_DEBUG"
+ private val TEST1 = ViewerConfigParser.ConfigEntry("test1", LogLevel.INFO.name, TAG1)
+ private val TEST2 = ViewerConfigParser.ConfigEntry("test2", LogLevel.DEBUG.name, TAG2)
+ private val TEST3 = ViewerConfigParser.ConfigEntry("test3", LogLevel.ERROR.name, TAG2)
+ private val GROUP1 = LogGroup("TEST_GROUP", true, true, TAG1)
+ private val GROUP2 = LogGroup("DEBUG_GROUP", true, true, TAG2)
+ private val GROUP3 = LogGroup("DEBUG_GROUP", true, true, TAG2)
+ private const val PATH = "/tmp/test.java"
+ }
+
+ private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java)
+ private val configBuilder = ViewerConfigBuilder(processor)
+ private val dummyCompilationUnit = CompilationUnit()
+
+ private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+ private fun parseConfig(json: String): Map<Int, ViewerConfigParser.ConfigEntry> {
+ return ViewerConfigParser().parseConfig(JsonReader(StringReader(json)))
+ }
+
+ @Test
+ fun processClass() {
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ GROUP1)
+ visitor.processCall(MethodCallExpr(), TEST2.messageString, LogLevel.DEBUG,
+ GROUP2)
+ visitor.processCall(MethodCallExpr(), TEST3.messageString, LogLevel.ERROR,
+ GROUP3)
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ configBuilder.processClass(dummyCompilationUnit, PATH)
+
+ val parsedConfig = parseConfig(configBuilder.build())
+ assertEquals(3, parsedConfig.size)
+ assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH,
+ TEST1.messageString, LogLevel.INFO, GROUP1)])
+ assertEquals(TEST2, parsedConfig[CodeUtils.hash(PATH, TEST2.messageString,
+ LogLevel.DEBUG, GROUP2)])
+ assertEquals(TEST3, parsedConfig[CodeUtils.hash(PATH, TEST3.messageString,
+ LogLevel.ERROR, GROUP3)])
+ }
+
+ @Test
+ fun processClass_nonUnique() {
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ GROUP1)
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ GROUP1)
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ GROUP1)
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ configBuilder.processClass(dummyCompilationUnit, PATH)
+
+ val parsedConfig = parseConfig(configBuilder.build())
+ assertEquals(1, parsedConfig.size)
+ assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString,
+ LogLevel.INFO, GROUP1)])
+ }
+
+ @Test
+ fun processClass_disabled() {
+ Mockito.`when`(processor.process(any(CompilationUnit::class.java),
+ any(ProtoLogCallVisitor::class.java), any(String::class.java)))
+ .thenAnswer { invocation ->
+ val visitor = invocation.arguments[1] as ProtoLogCallVisitor
+
+ visitor.processCall(MethodCallExpr(), TEST1.messageString, LogLevel.INFO,
+ GROUP1)
+ visitor.processCall(MethodCallExpr(), TEST2.messageString, LogLevel.DEBUG,
+ LogGroup("DEBUG_GROUP", false, true, TAG2))
+ visitor.processCall(MethodCallExpr(), TEST3.messageString, LogLevel.ERROR,
+ LogGroup("DEBUG_GROUP", true, false, TAG2))
+
+ invocation.arguments[0] as CompilationUnit
+ }
+
+ configBuilder.processClass(dummyCompilationUnit, PATH)
+
+ val parsedConfig = parseConfig(configBuilder.build())
+ assertEquals(2, parsedConfig.size)
+ assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString,
+ LogLevel.INFO, GROUP1)])
+ assertEquals(TEST3, parsedConfig[CodeUtils.hash(PATH, TEST3.messageString,
+ LogLevel.ERROR, LogGroup("DEBUG_GROUP", true, false, TAG2))])
+ }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt
new file mode 100644
index 000000000000..dc3ef7c57b35
--- /dev/null
+++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigParserTest.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.json.stream.JsonReader
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.StringReader
+
+class ViewerConfigParserTest {
+ private val parser = ViewerConfigParser()
+
+ private fun getJSONReader(str: String): JsonReader {
+ return JsonReader(StringReader(str))
+ }
+
+ @Test
+ fun parseMessage() {
+ val json = """
+ {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ """
+ val msg = parser.parseMessage(getJSONReader(json))
+ assertEquals("Test completed successfully: %b", msg.messageString)
+ assertEquals("ERROR", msg.level)
+ assertEquals("GENERIC_WM", msg.groupName)
+ }
+
+ @Test
+ fun parseMessage_reorder() {
+ val json = """
+ {
+ "group": "GENERIC_WM",
+ "level": "ERROR",
+ "message": "Test completed successfully: %b"
+ }
+ """
+ val msg = parser.parseMessage(getJSONReader(json))
+ assertEquals("Test completed successfully: %b", msg.messageString)
+ assertEquals("ERROR", msg.level)
+ assertEquals("GENERIC_WM", msg.groupName)
+ }
+
+ @Test
+ fun parseMessage_unknownEntry() {
+ val json = """
+ {
+ "unknown": "unknown entries should not block parsing",
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ """
+ val msg = parser.parseMessage(getJSONReader(json))
+ assertEquals("Test completed successfully: %b", msg.messageString)
+ assertEquals("ERROR", msg.level)
+ assertEquals("GENERIC_WM", msg.groupName)
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessage_noMessage() {
+ val json = """
+ {
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ """
+ parser.parseMessage(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessage_noLevel() {
+ val json = """
+ {
+ "message": "Test completed successfully: %b",
+ "group": "GENERIC_WM"
+ }
+ """
+ parser.parseMessage(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessage_noGroup() {
+ val json = """
+ {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR"
+ }
+ """
+ parser.parseMessage(getJSONReader(json))
+ }
+
+ @Test
+ fun parseGroup() {
+ val json = """
+ {
+ "tag": "WindowManager"
+ }
+ """
+ val group = parser.parseGroup(getJSONReader(json))
+ assertEquals("WindowManager", group.tag)
+ }
+
+ @Test
+ fun parseGroup_unknownEntry() {
+ val json = """
+ {
+ "unknown": "unknown entries should not block parsing",
+ "tag": "WindowManager"
+ }
+ """
+ val group = parser.parseGroup(getJSONReader(json))
+ assertEquals("WindowManager", group.tag)
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseGroup_noTag() {
+ val json = """
+ {
+ }
+ """
+ parser.parseGroup(getJSONReader(json))
+ }
+
+ @Test
+ fun parseMessages() {
+ val json = """
+ {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ },
+ "1792430067": {
+ "message": "Attempted to add window to a display that does not exist: %d. Aborting.",
+ "level": "WARN",
+ "group": "ERROR_WM"
+ }
+ }
+ """
+ val messages = parser.parseMessages(getJSONReader(json))
+ assertEquals(2, messages.size)
+ val msg1 =
+ ViewerConfigParser.MessageEntry("Test completed successfully: %b",
+ "ERROR", "GENERIC_WM")
+ val msg2 =
+ ViewerConfigParser.MessageEntry("Attempted to add window to a display that " +
+ "does not exist: %d. Aborting.", "WARN", "ERROR_WM")
+
+ assertEquals(msg1, messages[70933285])
+ assertEquals(msg2, messages[1792430067])
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseMessages_invalidHash() {
+ val json = """
+ {
+ "invalid": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ }
+ """
+ parser.parseMessages(getJSONReader(json))
+ }
+
+ @Test
+ fun parseGroups() {
+ val json = """
+ {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ },
+ "ERROR_WM": {
+ "tag": "WindowManagerError"
+ }
+ }
+ """
+ val groups = parser.parseGroups(getJSONReader(json))
+ assertEquals(2, groups.size)
+ val grp1 = ViewerConfigParser.GroupEntry("WindowManager")
+ val grp2 = ViewerConfigParser.GroupEntry("WindowManagerError")
+ assertEquals(grp1, groups["GENERIC_WM"])
+ assertEquals(grp2, groups["ERROR_WM"])
+ }
+
+ @Test
+ fun parseConfig() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ val config = parser.parseConfig(getJSONReader(json))
+ assertEquals(1, config.size)
+ val cfg1 = ViewerConfigParser.ConfigEntry("Test completed successfully: %b",
+ "ERROR", "WindowManager")
+ assertEquals(cfg1, config[70933285])
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_invalidVersion() {
+ val json = """
+ {
+ "version": "invalid",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_noVersion() {
+ val json = """
+ {
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_noMessages() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "groups": {
+ "GENERIC_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_noGroups() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+
+ @Test(expected = InvalidViewerConfigException::class)
+ fun parseConfig_missingGroup() {
+ val json = """
+ {
+ "version": "${Constants.VERSION}",
+ "messages": {
+ "70933285": {
+ "message": "Test completed successfully: %b",
+ "level": "ERROR",
+ "group": "GENERIC_WM"
+ }
+ },
+ "groups": {
+ "ERROR_WM": {
+ "tag": "WindowManager"
+ }
+ }
+ }
+ """
+ parser.parseConfig(getJSONReader(json))
+ }
+}
diff --git a/tools/validatekeymaps/Main.cpp b/tools/validatekeymaps/Main.cpp
index 56a242f1daaf..5ac9dfd2a557 100644
--- a/tools/validatekeymaps/Main.cpp
+++ b/tools/validatekeymaps/Main.cpp
@@ -137,7 +137,6 @@ static bool validateFile(const char* filename) {
}
}
- log("No errors.\n\n");
return true;
}