Update ProtoLogTool

Generate the impl classes and support writing the viewer config to a proto file

Flag: ACONFIG android.tracing.Flags.perfettoProtolog DEVELOPMENT
Bug: 276432490
Test: atest FrameworksServicesTests
Change-Id: I69956b5ef2b8b48a98860aa3b8579521480d7bc0
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index aa30a77..49c70b9 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -19,6 +19,8 @@
 import android.annotation.Nullable;
 import android.util.Slog;
 
+import com.android.internal.protolog.common.ILogger;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -39,10 +41,10 @@
  */
 public class ProtoLogViewerConfigReader {
     private static final String TAG = "ProtoLogViewerConfigReader";
-    private Map<Integer, String> mLogMessageMap = null;
+    private Map<Long, String> mLogMessageMap = null;
 
     /** Returns message format string for its hash or null if unavailable. */
-    public synchronized String getViewerString(int messageHash) {
+    public synchronized String getViewerString(long messageHash) {
         if (mLogMessageMap != null) {
             return mLogMessageMap.get(messageHash);
         } else {
@@ -53,19 +55,19 @@
     /**
      * Reads the specified viewer configuration file. Does nothing if the config is already loaded.
      */
-    public synchronized void loadViewerConfig(PrintWriter pw, String viewerConfigFilename) {
+    public synchronized void loadViewerConfig(ILogger logger, String viewerConfigFilename) {
         try {
             loadViewerConfig(new GZIPInputStream(new FileInputStream(viewerConfigFilename)));
-            logAndPrintln(pw, "Loaded " + mLogMessageMap.size()
+            logger.log("Loaded " + mLogMessageMap.size()
                     + " log definitions from " + viewerConfigFilename);
         } catch (FileNotFoundException e) {
-            logAndPrintln(pw, "Unable to load log definitions: File "
+            logger.log("Unable to load log definitions: File "
                     + viewerConfigFilename + " not found." + e);
         } catch (IOException e) {
-            logAndPrintln(pw, "Unable to load log definitions: IOException while reading "
+            logger.log("Unable to load log definitions: IOException while reading "
                     + viewerConfigFilename + ". " + e);
         } catch (JSONException e) {
-            logAndPrintln(pw, "Unable to load log definitions: JSON parsing exception while reading "
+            logger.log("Unable to load log definitions: JSON parsing exception while reading "
                     + viewerConfigFilename + ". " + e);
         }
     }
@@ -95,7 +97,7 @@
         while (it.hasNext()) {
             String key = (String) it.next();
             try {
-                int hash = Integer.parseInt(key);
+                long hash = Long.parseLong(key);
                 JSONObject val = messages.getJSONObject(key);
                 String msg = val.getString("message");
                 mLogMessageMap.put(hash, msg);
diff --git a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
new file mode 100644
index 0000000..ffd0d76
--- /dev/null
+++ b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.internal.protolog.common;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface ProtoLogToolInjected {
+    enum Value { VIEWER_CONFIG_PATH, LEGACY_OUTPUT_FILE_PATH, LEGACY_VIEWER_CONFIG_PATH }
+
+    Value value();
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
index ae50216..cedf259 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
@@ -98,7 +98,7 @@
 
     @Test
     public void loadViewerConfig() {
-        mConfig.loadViewerConfig(null, mTestViewerConfig.getAbsolutePath());
+        mConfig.loadViewerConfig(msg -> {}, mTestViewerConfig.getAbsolutePath());
         assertEquals("Test completed successfully: %b", mConfig.getViewerString(70933285));
         assertEquals("Test 2", mConfig.getViewerString(1352021864));
         assertEquals("Window %s is already added", mConfig.getViewerString(409412266));
@@ -107,7 +107,7 @@
 
     @Test
     public void loadViewerConfig_invalidFile() {
-        mConfig.loadViewerConfig(null, "/tmp/unknown/file/does/not/exist");
+        mConfig.loadViewerConfig(msg -> {}, "/tmp/unknown/file/does/not/exist");
         // No exception is thrown.
         assertNull(mConfig.getViewerString(1));
     }
diff --git a/tools/protologtool/src/com/android/protolog/tool/MethodCallVisitor.kt b/tools/protologtool/src/com/android/protolog/tool/MethodCallVisitor.kt
new file mode 100644
index 0000000..fda6351
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/MethodCallVisitor.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.github.javaparser.ast.expr.MethodCallExpr
+
+interface MethodCallVisitor {
+    fun processCall(call: MethodCallExpr)
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
index 9a76a6f..47724b7 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessor.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,115 +16,13 @@
 
 package com.android.protolog.tool
 
-import com.android.internal.protolog.common.LogLevel
 import com.github.javaparser.ast.CompilationUnit
-import com.github.javaparser.ast.Node
-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>,
+interface ProtoLogCallProcessor {
+    fun process(
+        code: CompilationUnit,
+        logCallVisitor: ProtoLogCallVisitor?,
+        otherCallVisitor: MethodCallVisitor?,
         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, getLevelForMethodName(
-                            call.name.toString(), call, context), groupMap.getValue(groupName))
-                }
-        return code
-    }
-
-    companion object {
-        fun getLevelForMethodName(name: String, node: Node, context: ParsingContext): LogLevel {
-            return when (name) {
-                "d" -> LogLevel.DEBUG
-                "v" -> LogLevel.VERBOSE
-                "i" -> LogLevel.INFO
-                "w" -> LogLevel.WARN
-                "e" -> LogLevel.ERROR
-                "wtf" -> LogLevel.WTF
-                else ->
-                    throw InvalidProtoLogCallException("Unknown log level $name in $node", context)
-            }
-        }
-    }
+    ): CompilationUnit
 }
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt
new file mode 100644
index 0000000..1087ae6
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.internal.protolog.common.LogLevel
+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.
+ */
+class ProtoLogCallProcessorImpl(
+    private val protoLogClassName: String,
+    private val protoLogGroupClassName: String,
+    private val groupMap: Map<String, LogGroup>
+) : ProtoLogCallProcessor {
+    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())
+    }
+
+    fun process(code: CompilationUnit, logCallVisitor: ProtoLogCallVisitor?, fileName: String):
+            CompilationUnit {
+        return process(code, logCallVisitor, null, fileName)
+    }
+
+    override fun process(
+        code: CompilationUnit,
+        logCallVisitor: ProtoLogCallVisitor?,
+        otherCallVisitor: MethodCallVisitor?,
+        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)
+
+                    val logMethods = LogLevel.entries.map { it.shortCode }
+                    if (logMethods.contains(call.name.id)) {
+                        // Process a log 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)
+                        }
+
+                        logCallVisitor?.processCall(call, messageString, getLevelForMethodName(
+                            call.name.toString(), call, context), groupMap.getValue(groupName))
+                    } else {
+                        // Process non-log message calls
+                        otherCallVisitor?.processCall(call)
+                    }
+                }
+        return code
+    }
+
+    private fun getLevelForMethodName(
+        name: String,
+        node: MethodCallExpr,
+        context: ParsingContext
+    ): LogLevel = when (name) {
+            "d" -> LogLevel.DEBUG
+            "v" -> LogLevel.VERBOSE
+            "i" -> LogLevel.INFO
+            "w" -> LogLevel.WARN
+            "e" -> LogLevel.ERROR
+            "wtf" -> LogLevel.WTF
+            else ->
+                throw InvalidProtoLogCallException("Unknown log level $name in $node", context)
+        }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
index ce856cd..1381847 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt
@@ -16,13 +16,22 @@
 
 package com.android.protolog.tool
 
+import com.android.internal.protolog.common.LogLevel
+import com.android.internal.protolog.common.ProtoLog
+import com.android.internal.protolog.common.ProtoLogToolInjected
 import com.android.protolog.tool.CommandOptions.Companion.USAGE
 import com.github.javaparser.ParseProblemException
 import com.github.javaparser.ParserConfiguration
 import com.github.javaparser.StaticJavaParser
 import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.SimpleName
+import com.github.javaparser.ast.expr.StringLiteralExpr
 import java.io.File
 import java.io.FileInputStream
+import java.io.FileNotFoundException
 import java.io.FileOutputStream
 import java.io.OutputStream
 import java.time.LocalDateTime
@@ -30,9 +39,21 @@
 import java.util.concurrent.Executors
 import java.util.jar.JarOutputStream
 import java.util.zip.ZipEntry
+import kotlin.math.abs
+import kotlin.random.Random
 import kotlin.system.exitProcess
 
 object ProtoLogTool {
+    const val PROTOLOG_IMPL_SRC_PATH =
+        "frameworks/base/core/java/com/android/internal/protolog/ProtoLogImpl.java"
+
+    data class LogCall(
+        val messageString: String,
+        val logLevel: LogLevel,
+        val logGroup: LogGroup,
+        val position: String
+    )
+
     private fun showHelpAndExit() {
         println(USAGE)
         exitProcess(-1)
@@ -51,26 +72,40 @@
     }
 
     private fun processClasses(command: CommandOptions) {
+        val generationHash = abs(Random.nextInt())
+        // Need to generate a new impl class to inject static constants into the class.
+        val generatedProtoLogImplClass =
+            "com.android.internal.protolog.ProtoLogImpl_$generationHash"
+
         val groups = injector.readLogGroups(
                 command.protoLogGroupsJarArg,
                 command.protoLogGroupsClassNameArg)
         val out = injector.fileOutputStream(command.outputSourceJarArg)
         val outJar = JarOutputStream(out)
-        val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
-                command.protoLogGroupsClassNameArg, groups)
+        val processor = ProtoLogCallProcessorImpl(
+            command.protoLogClassNameArg,
+            command.protoLogGroupsClassNameArg,
+            groups)
+
+        val protologImplName = generatedProtoLogImplClass.split(".").last()
+        val protologImplPath = "gen/${generatedProtoLogImplClass.split(".")
+                .joinToString("/")}.java"
+        outJar.putNextEntry(zipEntry(protologImplPath))
+
+        outJar.write(generateProtoLogImpl(protologImplName, command.viewerConfigFilePathArg,
+            command.legacyViewerConfigFilePathArg, command.legacyOutputFilePath).toByteArray())
 
         val executor = newThreadPool()
 
         try {
             command.javaSourceArgs.map { path ->
                 executor.submitCallable {
-                    val transformer = SourceTransformer(command.protoLogImplClassNameArg,
-                            command.protoLogCacheClassNameArg, processor)
+                    val transformer = SourceTransformer(generatedProtoLogImplClass, processor)
                     val file = File(path)
                     val text = injector.readText(file)
                     val outSrc = try {
                         val code = tryParse(text, path)
-                        if (containsProtoLogText(text, command.protoLogClassNameArg)) {
+                        if (containsProtoLogText(text, ProtoLog::class.java.simpleName)) {
                             transformer.processClass(text, path, packagePath(file, code), code)
                         } else {
                             text
@@ -93,51 +128,77 @@
             executor.shutdown()
         }
 
-        val cacheSplit = command.protoLogCacheClassNameArg.split(".")
-        val cacheName = cacheSplit.last()
-        val cachePackage = cacheSplit.dropLast(1).joinToString(".")
-        val cachePath = "gen/${cacheSplit.joinToString("/")}.java"
-
-        outJar.putNextEntry(zipEntry(cachePath))
-        outJar.write(generateLogGroupCache(cachePackage, cacheName, groups,
-                command.protoLogImplClassNameArg, command.protoLogGroupsClassNameArg).toByteArray())
-
         outJar.close()
         out.close()
     }
 
-    fun generateLogGroupCache(
-        cachePackage: String,
-        cacheName: String,
-        groups: Map<String, LogGroup>,
-        protoLogImplClassName: String,
-        protoLogGroupsClassName: String
+    private fun generateProtoLogImpl(
+        protoLogImplGenName: String,
+        viewerConfigFilePath: String,
+        legacyViewerConfigFilePath: String?,
+        legacyOutputFilePath: String?,
     ): String {
-        val fields = groups.values.map {
-            "public static boolean ${it.name}_enabled = false;"
-        }.joinToString("\n")
+        val file = File(PROTOLOG_IMPL_SRC_PATH)
 
-        val updates = groups.values.map {
-            "${it.name}_enabled = " +
-                    "$protoLogImplClassName.isEnabled($protoLogGroupsClassName.${it.name});"
-        }.joinToString("\n")
+        val text = try {
+            injector.readText(file)
+        } catch (e: FileNotFoundException) {
+            throw RuntimeException("Expected to find '$PROTOLOG_IMPL_SRC_PATH' but file was not " +
+                    "included in source for the ProtoLog Tool to process.")
+        }
 
-        return """
-            package $cachePackage;
+        val code = tryParse(text, PROTOLOG_IMPL_SRC_PATH)
 
-            public class $cacheName {
-${fields.replaceIndent("                ")}
+        val classDeclarations = code.findAll(ClassOrInterfaceDeclaration::class.java)
+        require(classDeclarations.size == 1) { "Expected exactly one class declaration" }
+        val classDeclaration = classDeclarations[0]
 
-                static {
-                    $protoLogImplClassName.sCacheUpdater = $cacheName::update;
-                    update();
-                }
+        val classNameNode = classDeclaration.findFirst(SimpleName::class.java).get()
+        classNameNode.setId(protoLogImplGenName)
 
-                static void update() {
-${updates.replaceIndent("                    ")}
-                }
-            }
-        """.trimIndent()
+        injectConstants(classDeclaration,
+            viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath)
+
+        return code.toString()
+    }
+
+    private fun injectConstants(
+        classDeclaration: ClassOrInterfaceDeclaration,
+        viewerConfigFilePath: String,
+        legacyViewerConfigFilePath: String?,
+        legacyOutputFilePath: String?
+    ) {
+        classDeclaration.fields.forEach { field ->
+            field.getAnnotationByClass(ProtoLogToolInjected::class.java)
+                    .ifPresent { annotationExpr ->
+                        if (annotationExpr.isSingleMemberAnnotationExpr) {
+                            val valueName = annotationExpr.asSingleMemberAnnotationExpr()
+                                    .memberValue.asNameExpr().name.asString()
+                            when (valueName) {
+                                ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH.name -> {
+                                    field.setFinal(true)
+                                    field.variables.first()
+                                            .setInitializer(StringLiteralExpr(viewerConfigFilePath))
+                                }
+                                ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH.name -> {
+                                    field.setFinal(true)
+                                    field.variables.first()
+                                            .setInitializer(legacyOutputFilePath?.let {
+                                                StringLiteralExpr(it)
+                                            } ?: NullLiteralExpr())
+                                }
+                                ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH.name -> {
+                                    field.setFinal(true)
+                                    field.variables.first()
+                                            .setInitializer(legacyViewerConfigFilePath?.let {
+                                                StringLiteralExpr(it)
+                                            } ?: NullLiteralExpr())
+                                }
+                                else -> error("Unhandled ProtoLogToolInjected value: $valueName.")
+                            }
+                        }
+                    }
+        }
     }
 
     private fun tryParse(code: String, fileName: String): CompilationUnit {
@@ -145,24 +206,53 @@
             return StaticJavaParser.parse(code)
         } catch (ex: ParseProblemException) {
             val problem = ex.problems.first()
-            throw ParsingException("Java parsing erro" +
-                    "r: ${problem.verboseMessage}",
+            throw ParsingException("Java parsing error: ${problem.verboseMessage}",
                     ParsingContext(fileName, problem.location.orElse(null)
                             ?.begin?.range?.orElse(null)?.begin?.line
                             ?: 0))
         }
     }
 
+    class LogCallRegistry {
+        private val statements = mutableMapOf<LogCall, Long>()
+
+        fun addLogCalls(calls: List<LogCall>) {
+            calls.forEach { logCall ->
+                if (logCall.logGroup.enabled) {
+                    statements.putIfAbsent(logCall,
+                        CodeUtils.hash(logCall.position, logCall.messageString,
+                            logCall.logLevel, logCall.logGroup))
+                }
+            }
+        }
+
+        fun getStatements(): Map<LogCall, Long> {
+            return statements
+        }
+    }
+
+    interface ProtologViewerConfigBuilder {
+        fun build(statements: Map<LogCall, Long>): ByteArray
+    }
+
     private fun viewerConf(command: CommandOptions) {
         val groups = injector.readLogGroups(
                 command.protoLogGroupsJarArg,
                 command.protoLogGroupsClassNameArg)
-        val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
+        val processor = ProtoLogCallProcessorImpl(command.protoLogClassNameArg,
                 command.protoLogGroupsClassNameArg, groups)
-        val builder = ViewerConfigBuilder(processor)
+        val outputType = command.viewerConfigTypeArg
+
+        val configBuilder: ProtologViewerConfigBuilder = when (outputType.lowercase()) {
+            "json" -> ViewerConfigJsonBuilder()
+            "proto" -> ViewerConfigProtoBuilder()
+            else -> error("Invalid output type provide. Provided '$outputType'.")
+        }
 
         val executor = newThreadPool()
 
+        val logCallRegistry = LogCallRegistry()
+
         try {
             command.javaSourceArgs.map { path ->
                 executor.submitCallable {
@@ -171,7 +261,7 @@
                     if (containsProtoLogText(text, command.protoLogClassNameArg)) {
                         try {
                             val code = tryParse(text, path)
-                            builder.findLogCalls(code, path, packagePath(file, code))
+                            findLogCalls(code, path, packagePath(file, code), processor)
                         } catch (ex: ParsingException) {
                             // If we cannot parse this file, skip it (and log why). Compilation will
                             // fail in a subsequent build step.
@@ -183,15 +273,38 @@
                     }
                 }
             }.forEach { future ->
-                builder.addLogCalls(future.get() ?: return@forEach)
+                logCallRegistry.addLogCalls(future.get() ?: return@forEach)
             }
         } finally {
             executor.shutdown()
         }
 
-        val out = injector.fileOutputStream(command.viewerConfigJsonArg)
-        out.write(builder.build().toByteArray())
-        out.close()
+        val outFile = injector.fileOutputStream(command.viewerConfigFileNameArg)
+        outFile.write(configBuilder.build(logCallRegistry.getStatements()))
+        outFile.close()
+    }
+
+    private fun findLogCalls(
+        unit: CompilationUnit,
+        path: String,
+        packagePath: String,
+        processor: ProtoLogCallProcessorImpl
+    ): List<LogCall> {
+        val calls = mutableListOf<LogCall>()
+        val logCallVisitor = object : ProtoLogCallVisitor {
+            override fun processCall(
+                call: MethodCallExpr,
+                messageString: String,
+                level: LogLevel,
+                group: LogGroup
+            ) {
+                val logCall = LogCall(messageString, level, group, packagePath)
+                calls.add(logCall)
+            }
+        }
+        processor.process(unit, logCallVisitor, path)
+
+        return calls
     }
 
     private fun packagePath(file: File, code: CompilationUnit): String {
@@ -204,7 +317,7 @@
     private fun read(command: CommandOptions) {
         LogParser(ViewerConfigParser())
                 .parse(FileInputStream(command.logProtofileArg),
-                        FileInputStream(command.viewerConfigJsonArg), System.out)
+                        FileInputStream(command.viewerConfigFileNameArg), System.out)
     }
 
     @JvmStatic
diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
index b50f357..2b71641 100644
--- a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt
@@ -22,11 +22,11 @@
 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.LongLiteralExpr
 import com.github.javaparser.ast.expr.MethodCallExpr
 import com.github.javaparser.ast.expr.NameExpr
 import com.github.javaparser.ast.expr.NullLiteralExpr
@@ -35,7 +35,6 @@
 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
@@ -45,15 +44,59 @@
 
 class SourceTransformer(
     protoLogImplClassName: String,
-    protoLogCacheClassName: String,
     private val protoLogCallProcessor: ProtoLogCallProcessor
-) : ProtoLogCallVisitor {
-    override fun processCall(
-        call: MethodCallExpr,
-        messageString: String,
-        level: LogLevel,
-        group: LogGroup
-    ) {
+) {
+    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)
+    }
+
+    fun processClass(
+        code: String,
+        path: String,
+        packagePath: String,
+        compilationUnit: CompilationUnit =
+            StaticJavaParser.parse(code)
+    ): String {
+        this.path = path
+        this.packagePath = packagePath
+        processedCode = code.split('\n').toMutableList()
+        offsets = IntArray(processedCode.size)
+        protoLogCallProcessor.process(compilationUnit, protoLogCallVisitor, otherCallVisitor, path)
+        return processedCode.joinToString("\n")
+    }
+
+    private val protoLogImplClassNode =
+            StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName)
+    private var processedCode: MutableList<String> = mutableListOf()
+    private var offsets: IntArray = IntArray(0)
+    /** The path of the file being processed, relative to $ANDROID_BUILD_TOP */
+    private var path: String = ""
+    /** The path of the file being processed, relative to the root package */
+    private var packagePath: String = ""
+
+    private val protoLogCallVisitor = object : ProtoLogCallVisitor {
+        override fun processCall(
+            call: MethodCallExpr,
+            messageString: String,
+            level: LogLevel,
+            group: LogGroup
+        ) {
+            validateCall(call)
+            val processedCallStatement =
+                createProcessedCallStatement(call, group, level, messageString)
+            val parentStmt = call.parentNode.get() as ExpressionStmt
+            injectProcessedCallStatementInCode(processedCallStatement, parentStmt)
+        }
+    }
+
+    private fun validateCall(call: MethodCallExpr) {
         // Input format: ProtoLog.e(GROUP, "msg %d", arg)
         if (!call.parentNode.isPresent) {
             // Should never happen
@@ -71,89 +114,79 @@
             throw RuntimeException("Unable to process log call $call " +
                     "- no grandparent node in AST")
         }
-        val ifStmt: IfStmt
-        if (group.enabled) {
-            val hash = CodeUtils.hash(packagePath, messageString, level, group)
-            val newCall = call.clone()
-            if (!group.textEnabled) {
-                // Remove message string if text logging is not enabled by default.
-                // Out: ProtoLog.e(GROUP, null, arg)
-                newCall.arguments[1].replace(NameExpr("null"))
-            }
-            // Insert message string hash as a second argument.
-            // Out: ProtoLog.e(GROUP, 1234, null, arg)
-            newCall.arguments.add(1, IntegerLiteralExpr(hash))
-            val argTypes = LogDataType.parseFormatString(messageString)
-            val typeMask = LogDataType.logDataTypesToBitMask(argTypes)
-            // Insert bitmap representing which Number parameters are to be considered as
-            // floating point numbers.
-            // Out: ProtoLog.e(GROUP, 1234, 0, null, arg)
-            newCall.arguments.add(2, IntegerLiteralExpr(typeMask))
-            // Replace call to a stub method with an actual implementation.
-            // Out: ProtoLogImpl.e(GROUP, 1234, null, arg)
-            newCall.setScope(protoLogImplClassNode)
-            // Create a call to ProtoLog$Cache.GROUP_enabled
-            // Out: com.android.server.protolog.ProtoLog$Cache.GROUP_enabled
-            val isLogEnabled = FieldAccessExpr(protoLogCacheClassNode, "${group.name}_enabled")
-            if (argTypes.size != call.arguments.size - 2) {
-                throw InvalidProtoLogCallException(
-                        "Number of arguments (${argTypes.size} does not mach format" +
-                                " string in: $call", ParsingContext(path, call))
-            }
-            val blockStmt = BlockStmt()
-            if (argTypes.isNotEmpty()) {
-                // Assign every argument to a variable to check its type in compile time
-                // (this is assignment is optimized-out by dex tool, there is no runtime impact)/
-                // Out: long protoLogParam0 = arg
-                argTypes.forEachIndexed { idx, type ->
-                    val varName = "protoLogParam$idx"
-                    val declaration = VariableDeclarator(getASTTypeForDataType(type), varName,
-                            getConversionForType(type)(newCall.arguments[idx + 4].clone()))
-                    blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration)))
-                    newCall.setArgument(idx + 4, NameExpr(SimpleName(varName)))
-                }
-            } else {
-                // Assign (Object[])null as the vararg parameter to prevent allocating an empty
-                // object array.
-                val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr())
-                newCall.addArgument(nullArray)
-            }
-            blockStmt.addStatement(ExpressionStmt(newCall))
-            // Create an IF-statement with the previously created condition.
-            // Out: if (ProtoLogImpl.isEnabled(GROUP)) {
-            //          long protoLogParam0 = arg;
-            //          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 ")
+    }
+
+    private fun createProcessedCallStatement(
+        call: MethodCallExpr,
+        group: LogGroup,
+        level: LogLevel,
+        messageString: String
+    ): BlockStmt {
+        val hash = CodeUtils.hash(packagePath, messageString, level, group)
+        val newCall = call.clone()
+        if (!group.textEnabled) {
+            // Remove message string if text logging is not enabled by default.
+            // Out: ProtoLog.e(GROUP, null, arg)
+            newCall.arguments[1].replace(NameExpr("null"))
         }
+        // Insert message string hash as a second argument.
+        // Out: ProtoLog.e(GROUP, 1234, null, arg)
+        newCall.arguments.add(1, LongLiteralExpr("" + hash + "L"))
+        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: ProtoLogImpl.e(GROUP, 1234, null, arg)
+        newCall.setScope(protoLogImplClassNode)
+        if (argTypes.size != call.arguments.size - 2) {
+            throw InvalidProtoLogCallException(
+                "Number of arguments (${argTypes.size} does not match format" +
+                        " string in: $call", ParsingContext(path, call))
+        }
+        val blockStmt = BlockStmt()
+        if (argTypes.isNotEmpty()) {
+            // Assign every argument to a variable to check its type in compile time
+            // (this is assignment is optimized-out by dex tool, there is no runtime impact)/
+            // Out: long protoLogParam0 = arg
+            argTypes.forEachIndexed { idx, type ->
+                val varName = "protoLogParam$idx"
+                val declaration = VariableDeclarator(getASTTypeForDataType(type), varName,
+                    getConversionForType(type)(newCall.arguments[idx + 4].clone()))
+                blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration)))
+                newCall.setArgument(idx + 4, NameExpr(SimpleName(varName)))
+            }
+        } else {
+            // Assign (Object[])null as the vararg parameter to prevent allocating an empty
+            // object array.
+            val nullArray = CastExpr(ArrayType(objectType), NullLiteralExpr())
+            newCall.addArgument(nullArray)
+        }
+        blockStmt.addStatement(ExpressionStmt(newCall))
+
+        return blockStmt
+    }
+
+    private fun injectProcessedCallStatementInCode(
+        processedCallStatement: BlockStmt,
+        parentStmt: ExpressionStmt
+    ) {
         // Inline the new statement.
-        val printedIfStmt = inlinePrinter.print(ifStmt)
+        val printedBlockStmt = inlinePrinter.print(processedCallStatement)
         // Append blank lines to preserve line numbering in file (to allow debugging)
         val parentRange = parentStmt.range.get()
         val newLines = parentRange.end.line - parentRange.begin.line
-        val newStmt = printedIfStmt.substringBeforeLast('}') + ("\n".repeat(newLines)) + '}'
+        val newStmt = printedBlockStmt.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 " +
+            throw RuntimeException("Unable to process log call in $parentStmt " +
                     "- unable to replace the call.")
         }
         val range = parentStmt.range.get()
@@ -161,29 +194,38 @@
         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)
+            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")
+    private val otherCallVisitor = object : MethodCallVisitor {
+        override fun processCall(call: MethodCallExpr) {
+            val newCall = call.clone()
+            newCall.setScope(protoLogImplClassNode)
 
-    init {
-        val config = PrettyPrinterConfiguration()
-        config.endOfLineCharacter = " "
-        config.indentSize = 0
-        config.tabWidth = 1
-        inlinePrinter = PrettyPrinter(config)
+            val range = call.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], newCall.toString())
+            newCode.split("\n").forEachIndexed { idx, line ->
+                offsets[begin + idx] += line.length - processedCode[begin + idx].length
+                processedCode[begin + idx] = line
+            }
+        }
     }
 
     companion object {
         private val stringType: ClassOrInterfaceType =
-                StaticJavaParser.parseClassOrInterfaceType("String")
+            StaticJavaParser.parseClassOrInterfaceType("String")
 
         fun getASTTypeForDataType(type: Int): Type {
             return when (type) {
@@ -202,36 +244,10 @@
             return when (type) {
                 LogDataType.STRING -> { expr ->
                     MethodCallExpr(TypeExpr(StaticJavaParser.parseClassOrInterfaceType("String")),
-                            SimpleName("valueOf"), NodeList(expr))
+                        SimpleName("valueOf"), NodeList(expr))
                 }
                 else -> { expr -> expr }
             }
         }
     }
-
-    private val protoLogImplClassNode =
-            StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName)
-    private val protoLogCacheClassNode =
-            StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogCacheClassName)
-    private var processedCode: MutableList<String> = mutableListOf()
-    private var offsets: IntArray = IntArray(0)
-    /** The path of the file being processed, relative to $ANDROID_BUILD_TOP */
-    private var path: String = ""
-    /** The path of the file being processed, relative to the root package */
-    private var packagePath: String = ""
-
-    fun processClass(
-        code: String,
-        path: String,
-        packagePath: String,
-        compilationUnit: CompilationUnit =
-               StaticJavaParser.parse(code)
-    ): String {
-        this.path = path
-        this.packagePath = packagePath
-        processedCode = code.split('\n').toMutableList()
-        offsets = IntArray(processedCode.size)
-        protoLogCallProcessor.process(compilationUnit, this, path)
-        return processedCode.joinToString("\n")
-    }
 }
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt
deleted file mode 100644
index 0d5d022..0000000
--- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigBuilder.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.protolog.tool
-
-import com.android.internal.protolog.common.LogLevel
-import com.android.json.stream.JsonWriter
-import com.github.javaparser.ast.CompilationUnit
-import com.android.protolog.tool.Constants.VERSION
-import com.github.javaparser.ast.expr.MethodCallExpr
-import java.io.StringWriter
-
-class ViewerConfigBuilder(
-    private val processor: ProtoLogCallProcessor
-) {
-    private fun addLogCall(logCall: LogCall, context: ParsingContext) {
-        val group = logCall.logGroup
-        val messageString = logCall.messageString
-        if (group.enabled) {
-            val key = logCall.key()
-            if (statements.containsKey(key)) {
-                if (statements[key] != logCall) {
-                    throw HashCollisionException(
-                            "Please modify the log message \"$messageString\" " +
-                                    "or \"${statements[key]}\" - their hashes are equal.", context)
-                }
-            } else {
-                groups.add(group)
-                statements[key] = logCall
-            }
-        }
-    }
-
-    private val statements: MutableMap<Int, LogCall> = mutableMapOf()
-    private val groups: MutableSet<LogGroup> = mutableSetOf()
-
-    fun findLogCalls(
-        unit: CompilationUnit,
-        path: String,
-        packagePath: String
-    ): List<Pair<LogCall, ParsingContext>> {
-        val calls = mutableListOf<Pair<LogCall, ParsingContext>>()
-        val visitor = object : ProtoLogCallVisitor {
-            override fun processCall(
-                call: MethodCallExpr,
-                messageString: String,
-                level: LogLevel,
-                group: LogGroup
-            ) {
-                val logCall = LogCall(messageString, level, group, packagePath)
-                val context = ParsingContext(path, call)
-                calls.add(logCall to context)
-            }
-        }
-        processor.process(unit, visitor, path)
-
-        return calls
-    }
-
-    fun addLogCalls(calls: List<Pair<LogCall, ParsingContext>>) {
-        calls.forEach { (logCall, context) ->
-            addLogCall(logCall, context)
-        }
-    }
-
-    fun build(): String {
-        val stringWriter = StringWriter()
-        val writer = JsonWriter(stringWriter)
-        writer.setIndent("  ")
-        writer.beginObject()
-        writer.name("version")
-        writer.value(VERSION)
-        writer.name("messages")
-        writer.beginObject()
-        statements.toSortedMap().forEach { (key, value) ->
-            writer.name(key.toString())
-            writer.beginObject()
-            writer.name("message")
-            writer.value(value.messageString)
-            writer.name("level")
-            writer.value(value.logLevel.name)
-            writer.name("group")
-            writer.value(value.logGroup.name)
-            writer.name("at")
-            writer.value(value.position)
-            writer.endObject()
-        }
-        writer.endObject()
-        writer.name("groups")
-        writer.beginObject()
-        groups.toSortedSet(Comparator { o1, o2 -> o1.name.compareTo(o2.name) }).forEach { group ->
-            writer.name(group.name)
-            writer.beginObject()
-            writer.name("tag")
-            writer.value(group.tag)
-            writer.endObject()
-        }
-        writer.endObject()
-        writer.endObject()
-        stringWriter.buffer.append('\n')
-        return stringWriter.toString()
-    }
-
-    data class LogCall(
-        val messageString: String,
-        val logLevel: LogLevel,
-        val logGroup: LogGroup,
-        val position: String
-    ) {
-        fun key() = CodeUtils.hash(position, messageString, logLevel, logGroup)
-    }
-}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt
new file mode 100644
index 0000000..7714db2
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.protolog.tool
+
+import com.android.json.stream.JsonWriter
+import com.android.protolog.tool.Constants.VERSION
+import java.io.StringWriter
+
+class ViewerConfigJsonBuilder : ProtoLogTool.ProtologViewerConfigBuilder {
+    override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray {
+        val groups = statements.map { it.key.logGroup }.toSet()
+        val stringWriter = StringWriter()
+        val writer = JsonWriter(stringWriter)
+        writer.setIndent("  ")
+        writer.beginObject()
+        writer.name("version")
+        writer.value(VERSION)
+        writer.name("messages")
+        writer.beginObject()
+        statements.forEach { (log, key) ->
+            writer.name(key.toString())
+            writer.beginObject()
+            writer.name("message")
+            writer.value(log.messageString)
+            writer.name("level")
+            writer.value(log.logLevel.name)
+            writer.name("group")
+            writer.value(log.logGroup.name)
+            writer.name("at")
+            writer.value(log.position)
+            writer.endObject()
+        }
+        writer.endObject()
+        writer.name("groups")
+        writer.beginObject()
+        groups.toSortedSet { 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().toByteArray()
+    }
+}
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
index 7278db0..58be3a3 100644
--- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigParser.kt
@@ -63,12 +63,12 @@
         return GroupEntry(tag)
     }
 
-    fun parseMessages(jsonReader: JsonReader): Map<Int, MessageEntry> {
-        val config: MutableMap<Int, MessageEntry> = mutableMapOf()
+    fun parseMessages(jsonReader: JsonReader): Map<Long, MessageEntry> {
+        val config: MutableMap<Long, MessageEntry> = mutableMapOf()
         jsonReader.beginObject()
         while (jsonReader.hasNext()) {
             val key = jsonReader.nextName()
-            val hash = key.toIntOrNull()
+            val hash = key.toLongOrNull()
                     ?: throw InvalidViewerConfigException("Invalid key in messages viewer config")
             config[hash] = parseMessage(jsonReader)
         }
@@ -89,8 +89,8 @@
 
     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
+    open fun parseConfig(jsonReader: JsonReader): Map<Long, ConfigEntry> {
+        var messages: Map<Long, MessageEntry>? = null
         var groups: Map<String, GroupEntry>? = null
         var version: String? = null
 
diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt
new file mode 100644
index 0000000..cf0876a
--- /dev/null
+++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt
@@ -0,0 +1,63 @@
+/*
+ * 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 perfetto.protos.PerfettoTrace.ProtoLogLevel
+import perfetto.protos.PerfettoTrace.ProtoLogViewerConfig
+
+/**
+ * A builder class to construct the viewer configuration (i.e. mappings of protolog hashes to log
+ * message information used to decode the protolog messages) encoded as a proto message.
+ */
+class ViewerConfigProtoBuilder : ProtoLogTool.ProtologViewerConfigBuilder {
+    /**
+     * @return a byte array of a ProtoLogViewerConfig proto message encoding all the viewer
+     * configurations mapping protolog hashes to message information and log group information.
+     */
+    override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray {
+        val configBuilder = ProtoLogViewerConfig.newBuilder()
+
+        val groups = statements.map { it.key.logGroup }.toSet()
+        val groupIds = mutableMapOf<LogGroup, Int>()
+        groups.forEach {
+            groupIds.putIfAbsent(it, groupIds.size + 1)
+        }
+
+        groupIds.forEach { (group, id) ->
+            configBuilder.addGroups(ProtoLogViewerConfig.Group.newBuilder()
+                    .setId(id)
+                    .setName(group.name)
+                    .setTag(group.tag)
+                    .build())
+        }
+
+        statements.forEach { (log, key) ->
+            val groupId = groupIds[log.logGroup] ?: error("missing group id")
+
+            configBuilder.addMessages(
+                ProtoLogViewerConfig.MessageData.newBuilder()
+                        .setMessageId(key)
+                        .setMessage(log.messageString)
+                        .setLevel(
+                            ProtoLogLevel.forNumber(log.logLevel.ordinal + 1))
+                        .setGroupId(groupId)
+            )
+        }
+
+        return configBuilder.build().toByteArray()
+    }
+}
diff --git a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
index c9bcbe5..822118c 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
@@ -16,14 +16,16 @@
 
 package com.android.protolog.tool
 
+import com.android.protolog.tool.ProtoLogTool.PROTOLOG_IMPL_SRC_PATH
+import com.google.common.truth.Truth
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import java.io.File
 import java.io.FileNotFoundException
 import java.io.OutputStream
 import java.util.jar.JarInputStream
+import java.util.regex.Pattern
 import org.junit.Assert
-import org.junit.Assert.assertTrue
 import org.junit.Test
 
 class EndToEndTest {
@@ -31,7 +33,7 @@
     @Test
     fun e2e_transform() {
         val output = run(
-                src = "frameworks/base/org/example/Example.java" to """
+                srcs = mapOf("frameworks/base/org/example/Example.java" to """
                     package org.example;
                     import com.android.internal.protolog.common.ProtoLog;
                     import static com.android.internal.protolog.ProtoLogGroup.GROUP;
@@ -43,7 +45,7 @@
                             ProtoLog.d(GROUP, "Example: %s %d", argString, argInt);
                         }
                     }
-                """.trimIndent(),
+                """.trimIndent()),
                 logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"),
                 commandOptions = CommandOptions(arrayOf("transform-protolog-calls",
                         "--protolog-class", "com.android.internal.protolog.common.ProtoLog",
@@ -54,13 +56,18 @@
                         "frameworks/base/org/example/Example.java"))
         )
         val outSrcJar = assertLoadSrcJar(output, "out.srcjar")
-        assertTrue(" 2066303299," in outSrcJar["frameworks/base/org/example/Example.java"]!!)
+        Truth.assertThat(outSrcJar["frameworks/base/org/example/Example.java"])
+                .containsMatch(Pattern.compile("\\{ String protoLogParam0 = " +
+                        "String\\.valueOf\\(argString\\); long protoLogParam1 = argInt; " +
+                        "com\\.android\\.internal\\.protolog.ProtoLogImpl_.*\\.d\\(" +
+                        "GROUP, -6872339441335321086L, 4, null, protoLogParam0, protoLogParam1" +
+                        "\\); \\}"))
     }
 
     @Test
     fun e2e_viewerConfig() {
         val output = run(
-                src = "frameworks/base/org/example/Example.java" to """
+                srcs = mapOf("frameworks/base/org/example/Example.java" to """
                     package org.example;
                     import com.android.internal.protolog.common.ProtoLog;
                     import static com.android.internal.protolog.ProtoLogGroup.GROUP;
@@ -72,7 +79,7 @@
                             ProtoLog.d(GROUP, "Example: %s %d", argString, argInt);
                         }
                     }
-                """.trimIndent(),
+                """.trimIndent()),
                 logGroup = LogGroup("GROUP", true, false, "TAG_GROUP"),
                 commandOptions = CommandOptions(arrayOf("generate-viewer-config",
                         "--protolog-class", "com.android.internal.protolog.common.ProtoLog",
@@ -83,7 +90,16 @@
                         "frameworks/base/org/example/Example.java"))
         )
         val viewerConfigJson = assertLoadText(output, "out.json")
-        assertTrue("\"2066303299\"" in viewerConfigJson)
+        Truth.assertThat(viewerConfigJson).contains("""
+            "messages": {
+                "-6872339441335321086": {
+                  "message": "Example: %s %d",
+                  "level": "DEBUG",
+                  "group": "GROUP",
+                  "at": "org\/example\/Example.java"
+                }
+              }
+        """.trimIndent())
     }
 
     private fun assertLoadSrcJar(
@@ -111,21 +127,46 @@
     }
 
     fun run(
-        src: Pair<String, String>,
+        srcs: Map<String, String>,
         logGroup: LogGroup,
         commandOptions: CommandOptions
     ): Map<String, ByteArray> {
         val outputs = mutableMapOf<String, ByteArrayOutputStream>()
 
+        val srcs = srcs.toMutableMap()
+        srcs[PROTOLOG_IMPL_SRC_PATH] = """
+            package com.android.internal.protolog;
+
+            import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH;
+            import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH;
+            import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH;
+
+            import com.android.internal.protolog.common.ProtoLogToolInjected;
+
+            public class ProtoLogImpl {
+                @ProtoLogToolInjected(VIEWER_CONFIG_PATH)
+                private static String sViewerConfigPath;
+
+                @ProtoLogToolInjected(LEGACY_VIEWER_CONFIG_PATH)
+                private static String sLegacyViewerConfigPath;
+
+                @ProtoLogToolInjected(LEGACY_OUTPUT_FILE_PATH)
+                private static String sLegacyOutputFilePath;
+            }
+        """.trimIndent()
+
         ProtoLogTool.injector = object : ProtoLogTool.Injector {
             override fun fileOutputStream(file: String): OutputStream =
                     ByteArrayOutputStream().also { outputs[file] = it }
 
             override fun readText(file: File): String {
-                if (file.path == src.first) {
-                    return src.second
+                for (src in srcs.entries) {
+                    val filePath = src.key
+                    if (file.path == filePath) {
+                        return src.value
+                    }
                 }
-                throw FileNotFoundException("expected: ${src.first}, but was $file")
+                throw FileNotFoundException("$file not found in [${srcs.keys.joinToString()}].")
             }
 
             override fun readLogGroups(jarPath: String, className: String) = mapOf(
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
similarity index 96%
rename from tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt
rename to tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
index 90b8059..5e50f71 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
@@ -22,7 +22,7 @@
 import org.junit.Assert.assertEquals
 import org.junit.Test
 
-class ProtoLogCallProcessorTest {
+class ProtoLogCallProcessorImplTest {
     private data class LogCall(
         val call: MethodCallExpr,
         val messageString: String,
@@ -32,8 +32,11 @@
 
     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 visitor = ProtoLogCallProcessorImpl(
+        "org.example.ProtoLog",
+        "org.example.ProtoLogGroup",
+            groupMap
+    )
     private val processor = object : ProtoLogCallVisitor {
         override fun processCall(
             call: MethodCallExpr,
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt
deleted file mode 100644
index ea9a58d..0000000
--- a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogToolTest.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.protolog.tool
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class ProtoLogToolTest {
-
-    @Test
-    fun generateLogGroupCache() {
-        val groups = mapOf(
-                "GROUP1" to LogGroup("GROUP1", true, true, "TAG1"),
-                "GROUP2" to LogGroup("GROUP2", true, true, "TAG2")
-        )
-        val code = ProtoLogTool.generateLogGroupCache("org.example", "ProtoLog\$Cache",
-                groups, "org.example.ProtoLogImpl", "org.example.ProtoLogGroups")
-
-        assertEquals("""
-            package org.example;
-
-            public class ProtoLog${'$'}Cache {
-                public static boolean GROUP1_enabled = false;
-                public static boolean GROUP2_enabled = false;
-
-                static {
-                    org.example.ProtoLogImpl.sCacheUpdater = ProtoLog${'$'}Cache::update;
-                    update();
-                }
-
-                static void update() {
-                    GROUP1_enabled = org.example.ProtoLogImpl.isEnabled(org.example.ProtoLogGroups.GROUP1);
-                    GROUP2_enabled = org.example.ProtoLogImpl.isEnabled(org.example.ProtoLogGroups.GROUP2);
-                }
-            }
-        """.trimIndent(), code)
-    }
-}
\ No newline at end of file
diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
index f52bfec..de0b5ba 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt
@@ -20,17 +20,14 @@
 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 com.google.common.truth.Truth
 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;
 
@@ -79,7 +76,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+                    { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
                 }
             }
             """.trimIndent()
@@ -89,20 +86,20 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); 
+                    { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); 
             
             }
                 }
             }
             """.trimIndent()
 
-        private val TRANSFORMED_CODE_MULTICALL_TEXT_ENABLED = """
+        private val TRANSFORMED_CODE_MULTICALL_TEXT = """
             package org.example;
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
-                    if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+                    { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
+                    { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); }
                 }
             }
             """.trimIndent()
@@ -112,7 +109,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogCache.TEST_GROUP_enabled) { org.example.ProtoLogImpl.w(TEST_GROUP, -1741986185, 0, "test", (Object[]) null); }
+                    { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, "test", (Object[]) null); }
                 }
             }
             """.trimIndent()
@@ -122,7 +119,7 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, 1698911065, 9, null, protoLogParam0, protoLogParam1); }
+                    { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, null, protoLogParam0, protoLogParam1); }
                 }
             }
             """.trimIndent()
@@ -132,43 +129,19 @@
 
             class Test {
                 void test() {
-                    if (org.example.ProtoLogCache.TEST_GROUP_enabled) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, 1780316587, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); 
+                    { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); 
             
             }
                 }
             }
             """.trimIndent()
 
-        private val TRANSFORMED_CODE_DISABLED = """
-            package org.example;
-
-            class Test {
-                void test() {
-                    if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); }
-                }
-            }
-            """.trimIndent()
-
-        private val TRANSFORMED_CODE_MULTILINE_DISABLED = """
-            package org.example;
-
-            class Test {
-                void test() {
-                    if (false) { /* TEST_GROUP is disabled */ ProtoLog.w(TEST_GROUP, "test %d %f " + "abc %s\n test", 100, 0.1, "test"); 
-            
-            }
-                }
-            }
-            """.trimIndent()
-        /* ktlint-enable max-line-length */
-
         private const val PATH = "com.example.Test.java"
     }
 
     private val processor: ProtoLogCallProcessor = Mockito.mock(ProtoLogCallProcessor::class.java)
     private val implName = "org.example.ProtoLogImpl"
-    private val cacheName = "org.example.ProtoLogCache"
-    private val sourceJarWriter = SourceTransformer(implName, cacheName, processor)
+    private val sourceJarWriter = SourceTransformer(implName, processor)
 
     private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
 
@@ -176,9 +149,12 @@
     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 ->
+        Mockito.`when`(processor.process(
+            any(CompilationUnit::class.java),
+            any(ProtoLogCallVisitor::class.java),
+            any(MethodCallVisitor::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",
@@ -190,18 +166,15 @@
         val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(1, ifStmts.size)
-        val ifStmt = ifStmts[0]
-        assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
-        assertFalse(ifStmt.elseStmt.isPresent)
-        assertEquals(3, ifStmt.thenStmt.childNodes.size)
-        val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
-        assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+        val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+            it.scope.orElse(null)?.toString() == implName
+        }
+        Truth.assertThat(protoLogCalls).hasSize(1)
+        val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
         assertEquals(6, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
-        assertEquals("1698911065", methodCall.arguments[1].toString())
+        assertEquals("-1473209266730422156L", 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())
@@ -213,9 +186,12 @@
     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 ->
+        Mockito.`when`(processor.process(
+            any(CompilationUnit::class.java),
+            any(ProtoLogCallVisitor::class.java),
+            any(MethodCallVisitor::class.java),
+            any(String::class.java))
+        ).thenAnswer { invocation ->
             val visitor = invocation.arguments[1] as ProtoLogCallVisitor
 
             val calls = code.findAll(MethodCallExpr::class.java)
@@ -232,32 +208,32 @@
         val out = sourceJarWriter.processClass(TEST_CODE_MULTICALLS, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(3, ifStmts.size)
-        val ifStmt = ifStmts[1]
-        assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
-        assertFalse(ifStmt.elseStmt.isPresent)
-        assertEquals(3, ifStmt.thenStmt.childNodes.size)
-        val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
-        assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+        val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+            it.scope.orElse(null)?.toString() == implName
+        }
+        Truth.assertThat(protoLogCalls).hasSize(3)
+        val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
         assertEquals(6, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
-        assertEquals("1698911065", methodCall.arguments[1].toString())
+        assertEquals("-1473209266730422156L", 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)
+        assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT, 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 ->
+        Mockito.`when`(processor.process(
+            any(CompilationUnit::class.java),
+            any(ProtoLogCallVisitor::class.java),
+            any(MethodCallVisitor::class.java),
+            any(String::class.java))
+        ).thenAnswer { invocation ->
             val visitor = invocation.arguments[1] as ProtoLogCallVisitor
 
             visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
@@ -270,18 +246,15 @@
         val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(1, ifStmts.size)
-        val ifStmt = ifStmts[0]
-        assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
-        assertFalse(ifStmt.elseStmt.isPresent)
-        assertEquals(4, ifStmt.thenStmt.childNodes.size)
-        val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
-        assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+        val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+            it.scope.orElse(null)?.toString() == implName
+        }
+        Truth.assertThat(protoLogCalls).hasSize(1)
+        val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
         assertEquals(7, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
-        assertEquals("1780316587", methodCall.arguments[1].toString())
+        assertEquals("-4447034859795564700L", methodCall.arguments[1].toString())
         assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
         assertEquals("protoLogParam0", methodCall.arguments[4].toString())
         assertEquals("protoLogParam1", methodCall.arguments[5].toString())
@@ -293,9 +266,12 @@
     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 ->
+        Mockito.`when`(processor.process(
+            any(CompilationUnit::class.java),
+            any(ProtoLogCallVisitor::class.java),
+            any(MethodCallVisitor::class.java),
+            any(String::class.java))
+        ).thenAnswer { invocation ->
             val visitor = invocation.arguments[1] as ProtoLogCallVisitor
 
             visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test",
@@ -307,18 +283,15 @@
         val out = sourceJarWriter.processClass(TEST_CODE_NO_PARAMS, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(1, ifStmts.size)
-        val ifStmt = ifStmts[0]
-        assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
-        assertFalse(ifStmt.elseStmt.isPresent)
-        assertEquals(1, ifStmt.thenStmt.childNodes.size)
-        val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
-        assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+        val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+            it.scope.orElse(null)?.toString() == implName
+        }
+        Truth.assertThat(protoLogCalls).hasSize(1)
+        val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
         assertEquals(5, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
-        assertEquals("-1741986185", methodCall.arguments[1].toString())
+        assertEquals("3218600869538902408L", methodCall.arguments[1].toString())
         assertEquals(0.toString(), methodCall.arguments[2].toString())
         assertEquals(TRANSFORMED_CODE_NO_PARAMS, out)
     }
@@ -327,9 +300,12 @@
     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 ->
+        Mockito.`when`(processor.process(
+            any(CompilationUnit::class.java),
+            any(ProtoLogCallVisitor::class.java),
+            any(MethodCallVisitor::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",
@@ -341,18 +317,15 @@
         val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(1, ifStmts.size)
-        val ifStmt = ifStmts[0]
-        assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
-        assertFalse(ifStmt.elseStmt.isPresent)
-        assertEquals(3, ifStmt.thenStmt.childNodes.size)
-        val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[0] as MethodCallExpr
-        assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+        val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+            it.scope.orElse(null)?.toString() == implName
+        }
+        Truth.assertThat(protoLogCalls).hasSize(1)
+        val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
         assertEquals(6, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
-        assertEquals("1698911065", methodCall.arguments[1].toString())
+        assertEquals("-1473209266730422156L", methodCall.arguments[1].toString())
         assertEquals(0b1001.toString(), methodCall.arguments[2].toString())
         assertEquals("null", methodCall.arguments[3].toString())
         assertEquals("protoLogParam0", methodCall.arguments[4].toString())
@@ -364,9 +337,12 @@
     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 ->
+        Mockito.`when`(processor.process(
+            any(CompilationUnit::class.java),
+            any(ProtoLogCallVisitor::class.java),
+            any(MethodCallVisitor::class.java),
+            any(String::class.java))
+        ).thenAnswer { invocation ->
             val visitor = invocation.arguments[1] as ProtoLogCallVisitor
 
             visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
@@ -379,18 +355,15 @@
         val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
         code = StaticJavaParser.parse(out)
 
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(1, ifStmts.size)
-        val ifStmt = ifStmts[0]
-        assertEquals("$cacheName.TEST_GROUP_enabled", ifStmt.condition.toString())
-        assertFalse(ifStmt.elseStmt.isPresent)
-        assertEquals(4, ifStmt.thenStmt.childNodes.size)
-        val methodCall = ifStmt.thenStmt.findAll(MethodCallExpr::class.java)[1] as MethodCallExpr
-        assertEquals(PROTO_LOG_IMPL_PATH, methodCall.scope.get().toString())
+        val protoLogCalls = code.findAll(MethodCallExpr::class.java).filter {
+            it.scope.orElse(null)?.toString() == implName
+        }
+        Truth.assertThat(protoLogCalls).hasSize(1)
+        val methodCall = protoLogCalls[0] as MethodCallExpr
         assertEquals("w", methodCall.name.asString())
         assertEquals(7, methodCall.arguments.size)
         assertEquals("TEST_GROUP", methodCall.arguments[0].toString())
-        assertEquals("1780316587", methodCall.arguments[1].toString())
+        assertEquals("-4447034859795564700L", methodCall.arguments[1].toString())
         assertEquals(0b001001.toString(), methodCall.arguments[2].toString())
         assertEquals("null", methodCall.arguments[3].toString())
         assertEquals("protoLogParam0", methodCall.arguments[4].toString())
@@ -398,55 +371,4 @@
         assertEquals("protoLogParam2", methodCall.arguments[6].toString())
         assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out)
     }
-
-    @Test
-    fun processClass_disabled() {
-        var code = StaticJavaParser.parse(TEST_CODE)
-
-        Mockito.`when`(processor.process(any(CompilationUnit::class.java),
-                any(ProtoLogCallVisitor::class.java), any(String::class.java)))
-                .thenAnswer { invocation ->
-            val visitor = invocation.arguments[1] as ProtoLogCallVisitor
-
-            visitor.processCall(code.findAll(MethodCallExpr::class.java)[0], "test %d %f",
-                    LogLevel.WARN, LogGroup("TEST_GROUP", false, true, "WM_TEST"))
-
-            invocation.arguments[0] as CompilationUnit
-        }
-
-        val out = sourceJarWriter.processClass(TEST_CODE, PATH, PATH, code)
-        code = StaticJavaParser.parse(out)
-
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(1, ifStmts.size)
-        val ifStmt = ifStmts[0]
-        assertEquals("false", ifStmt.condition.toString())
-        assertEquals(TRANSFORMED_CODE_DISABLED, out)
-    }
-
-    @Test
-    fun processClass_disabledMultiline() {
-        var code = StaticJavaParser.parse(TEST_CODE_MULTILINE)
-
-        Mockito.`when`(processor.process(any(CompilationUnit::class.java),
-                any(ProtoLogCallVisitor::class.java), any(String::class.java)))
-                .thenAnswer { invocation ->
-            val visitor = invocation.arguments[1] as ProtoLogCallVisitor
-
-            visitor.processCall(code.findAll(MethodCallExpr::class.java)[0],
-                    "test %d %f abc %s\n test", LogLevel.WARN, LogGroup("TEST_GROUP",
-                    false, true, "WM_TEST"))
-
-            invocation.arguments[0] as CompilationUnit
-        }
-
-        val out = sourceJarWriter.processClass(TEST_CODE_MULTILINE, PATH, PATH, code)
-        code = StaticJavaParser.parse(out)
-
-        val ifStmts = code.findAll(IfStmt::class.java)
-        assertEquals(1, ifStmts.size)
-        val ifStmt = ifStmts[0]
-        assertEquals("false", ifStmt.condition.toString())
-        assertEquals(TRANSFORMED_CODE_MULTILINE_DISABLED, out)
-    }
 }
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt
similarity index 66%
rename from tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt
rename to tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt
index 52dce21..d27ae88 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigBuilderTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt
@@ -18,13 +18,12 @@
 
 import com.android.internal.protolog.common.LogLevel
 import com.android.json.stream.JsonReader
-import com.android.protolog.tool.ViewerConfigBuilder.LogCall
+import com.android.protolog.tool.ProtoLogTool.LogCall
+import java.io.StringReader
 import org.junit.Assert.assertEquals
 import org.junit.Test
-import org.mockito.Mockito
-import java.io.StringReader
 
-class ViewerConfigBuilderTest {
+class ViewerConfigJsonBuilderTest {
     companion object {
         private val TAG1 = "WM_TEST"
         private val TAG2 = "WM_DEBUG"
@@ -39,20 +38,22 @@
         private const val PATH = "/tmp/test.java"
     }
 
-    private val configBuilder = ViewerConfigBuilder(Mockito.mock(ProtoLogCallProcessor::class.java))
+    private val configBuilder = ViewerConfigJsonBuilder()
 
-    private fun parseConfig(json: String): Map<Int, ViewerConfigParser.ConfigEntry> {
+    private fun parseConfig(json: String): Map<Long, ViewerConfigParser.ConfigEntry> {
         return ViewerConfigParser().parseConfig(JsonReader(StringReader(json)))
     }
 
     @Test
     fun processClass() {
-        configBuilder.addLogCalls(listOf(
+        val logCallRegistry = ProtoLogTool.LogCallRegistry()
+        logCallRegistry.addLogCalls(listOf(
                 LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
                 LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP2, PATH),
-                LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH)).withContext())
+                LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH)))
 
-        val parsedConfig = parseConfig(configBuilder.build())
+        val parsedConfig = parseConfig(
+            configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8))
         assertEquals(3, parsedConfig.size)
         assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH,
 	           TEST1.messageString, LogLevel.INFO, GROUP1)])
@@ -64,32 +65,16 @@
 
     @Test
     fun processClass_nonUnique() {
-        configBuilder.addLogCalls(listOf(
+        val logCallRegistry = ProtoLogTool.LogCallRegistry()
+        logCallRegistry.addLogCalls(listOf(
                 LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
                 LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
-                LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH)).withContext())
+                LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH)))
 
-        val parsedConfig = parseConfig(configBuilder.build())
+        val parsedConfig = parseConfig(
+            configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8))
         assertEquals(1, parsedConfig.size)
         assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString,
-            	   LogLevel.INFO, GROUP1)])
+            LogLevel.INFO, GROUP1)])
     }
-
-    @Test
-    fun processClass_disabled() {
-        configBuilder.addLogCalls(listOf(
-                LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH),
-                LogCall(TEST2.messageString, LogLevel.DEBUG, GROUP_DISABLED, PATH),
-                LogCall(TEST3.messageString, LogLevel.ERROR, GROUP_TEXT_DISABLED, PATH))
-                .withContext())
-
-        val parsedConfig = parseConfig(configBuilder.build())
-        assertEquals(2, parsedConfig.size)
-        assertEquals(TEST1, parsedConfig[CodeUtils.hash(
-                PATH, TEST1.messageString, LogLevel.INFO, GROUP1)])
-        assertEquals(TEST3, parsedConfig[CodeUtils.hash(
-                PATH, TEST3.messageString, LogLevel.ERROR, GROUP_TEXT_DISABLED)])
-    }
-
-    private fun List<LogCall>.withContext() = map { it to ParsingContext() }
 }