diff --git a/Android.bp b/Android.bp
index 019bf65087743d166e25af8a5a77925ca6d5f2bc..5ada10d19f5de451b5ad4000570e520dc31ae036 100644
--- a/Android.bp
+++ b/Android.bp
@@ -386,7 +386,7 @@ java_defaults {
         // TODO(b/120066492): remove gps_debug and protolog.conf.json when the build
         // system propagates "required" properly.
         "gps_debug.conf",
-        "protolog.conf.json.gz",
+        "core.protolog.pb",
         "framework-res",
         // any install dependencies should go into framework-minus-apex-install-dependencies
         // rather than here to avoid bloating incremental build time
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 34b8a878b3d10b62c95418f3f5dee62d5302992a..184421518d32338ff4eaa1824f5f6951fe410158 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -528,23 +528,19 @@ filegroup {
     ],
 }
 
-// common protolog sources without classes that rely on Android SDK
+// PackageManager common
 filegroup {
-    name: "protolog-common-no-android-src",
+    name: "framework-pm-common-shared-srcs",
     srcs: [
-        ":protolog-common-src",
-    ],
-    exclude_srcs: [
-        "com/android/internal/protolog/common/ProtoLog.java",
+        "com/android/server/pm/pkg/AndroidPackage.java",
+        "com/android/server/pm/pkg/AndroidPackageSplit.java",
     ],
 }
 
-// PackageManager common
 filegroup {
-    name: "framework-pm-common-shared-srcs",
+    name: "protolog-impl",
     srcs: [
-        "com/android/server/pm/pkg/AndroidPackage.java",
-        "com/android/server/pm/pkg/AndroidPackageSplit.java",
+        "com/android/internal/protolog/ProtoLogImpl.java",
     ],
 }
 
@@ -554,7 +550,7 @@ java_library {
     srcs: [
         "com/android/internal/protolog/ProtoLogImpl.java",
         "com/android/internal/protolog/ProtoLogViewerConfigReader.java",
-        ":protolog-common-src",
+        ":perfetto_trace_javastream_protos",
     ],
 }
 
diff --git a/core/java/android/tracing/perfetto/DataSource.java b/core/java/android/tracing/perfetto/DataSource.java
index 4e08aeef88e6859899de43744c74cfb536c23a7a..d0c719b86ac9609cfaa2a8f48b415948282b8bd1 100644
--- a/core/java/android/tracing/perfetto/DataSource.java
+++ b/core/java/android/tracing/perfetto/DataSource.java
@@ -18,6 +18,8 @@ package android.tracing.perfetto;
 
 import android.util.proto.ProtoInputStream;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Templated base class meant to be derived by embedders to create a custom data
  * source.
@@ -87,7 +89,8 @@ public abstract class DataSource<DataSourceInstanceType extends DataSourceInstan
      *
      * NOTE: Should only be called from native side.
      */
-    protected TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) {
+    @VisibleForTesting
+    public TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) {
         return null;
     }
 
diff --git a/core/java/android/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java
index 3710b4df33e8bb7c0f22e420f9ec93a7f43f8ea1..904cf55e014ac3cd2c3c58f27398fe4a424105b9 100644
--- a/core/java/android/tracing/perfetto/DataSourceInstance.java
+++ b/core/java/android/tracing/perfetto/DataSourceInstance.java
@@ -16,6 +16,8 @@
 
 package android.tracing.perfetto;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * @hide
  */
@@ -66,7 +68,8 @@ public abstract class DataSourceInstance implements AutoCloseable {
      * Only required to be called when instance was retrieved with
      * `DataSource#getDataSourceInstanceLocked`.
      */
-    public final void release() {
+    @VisibleForTesting
+    public void release() {
         mDataSource.releaseDataSourceInstance(mInstanceIndex);
     }
 
diff --git a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
similarity index 81%
rename from core/java/com/android/internal/protolog/BaseProtoLogImpl.java
rename to core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index abe6c7c079c2a69b9107c96d73eaf6627c0bcf37..d9ac5a9fe47acaace61c2db011e24ab88426a661 100644
--- a/core/java/com/android/internal/protolog/BaseProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 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.
@@ -37,8 +37,11 @@ import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
 import com.android.internal.protolog.common.LogDataType;
+import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.util.TraceBuffer;
 
 import java.io.File;
@@ -48,52 +51,49 @@ import java.util.ArrayList;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
 
-
 /**
  * A service for the ProtoLog logging system.
  */
-public class BaseProtoLogImpl {
-    protected static final TreeMap<String, IProtoLogGroup> LOG_GROUPS = new TreeMap<>();
-
-    /**
-     * A runnable to update the cached output of {@link #isEnabled}.
-     *
-     * Must be invoked after every action that could change the result of {@link #isEnabled}, eg.
-     * starting / stopping proto log, or enabling / disabling log groups.
-     */
-    public static Runnable sCacheUpdater = () -> { };
-
-    protected static void addLogGroupEnum(IProtoLogGroup[] config) {
-        for (IProtoLogGroup group : config) {
-            LOG_GROUPS.put(group.name(), group);
-        }
-    }
+public class LegacyProtoLogImpl implements IProtoLog {
+    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
 
+    private static final int BUFFER_CAPACITY = 1024 * 1024;
+    private static final int PER_CHUNK_SIZE = 1024;
     private static final String TAG = "ProtoLog";
     private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
     static final String PROTOLOG_VERSION = "1.0.0";
     private static final int DEFAULT_PER_CHUNK_SIZE = 0;
 
     private final File mLogFile;
-    private final String mViewerConfigFilename;
+    private final String mLegacyViewerConfigFilename;
     private final TraceBuffer mBuffer;
-    protected final ProtoLogViewerConfigReader mViewerConfig;
+    private final LegacyProtoLogViewerConfigReader mViewerConfig;
     private final int mPerChunkSize;
 
     private boolean mProtoLogEnabled;
     private boolean mProtoLogEnabledLockFree;
     private final Object mProtoLogEnabledLock = new Object();
 
-    @VisibleForTesting
-    public enum LogLevel {
-        DEBUG, VERBOSE, INFO, WARN, ERROR, WTF
+    public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename) {
+        this(new File(outputFile), viewerConfigFilename, BUFFER_CAPACITY,
+                new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE);
+    }
+
+    public LegacyProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
+            LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize) {
+        mLogFile = file;
+        mBuffer = new TraceBuffer(bufferCapacity);
+        mLegacyViewerConfigFilename = viewerConfigFilename;
+        mViewerConfig = viewerConfig;
+        mPerChunkSize = perChunkSize;
     }
 
     /**
      * Main log method, do not call directly.
      */
     @VisibleForTesting
-    public void log(LogLevel level, IProtoLogGroup group, int messageHash, int paramsMask,
+    @Override
+    public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString, Object[] args) {
         if (group.isLogToProto()) {
             logToProto(messageHash, paramsMask, args);
@@ -103,7 +103,7 @@ public class BaseProtoLogImpl {
         }
     }
 
-    private void logToLogcat(String tag, LogLevel level, int messageHash,
+    private void logToLogcat(String tag, LogLevel level, long messageHash,
             @Nullable String messageString, Object[] args) {
         String message = null;
         if (messageString == null) {
@@ -157,7 +157,7 @@ public class BaseProtoLogImpl {
         }
     }
 
-    private void logToProto(int messageHash, int paramsMask, Object[] args) {
+    private void logToProto(long messageHash, int paramsMask, Object[] args) {
         if (!isProtoEnabled()) {
             return;
         }
@@ -219,20 +219,6 @@ public class BaseProtoLogImpl {
         }
     }
 
-    public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
-            ProtoLogViewerConfigReader viewerConfig) {
-        this(file, viewerConfigFilename, bufferCapacity, viewerConfig, DEFAULT_PER_CHUNK_SIZE);
-    }
-
-    public BaseProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity,
-            ProtoLogViewerConfigReader viewerConfig, int perChunkSize) {
-        mLogFile = file;
-        mBuffer = new TraceBuffer(bufferCapacity);
-        mViewerConfigFilename = viewerConfigFilename;
-        mViewerConfig = viewerConfig;
-        mPerChunkSize = perChunkSize;
-    }
-
     /**
      * Starts the logging a circular proto buffer.
      *
@@ -248,7 +234,6 @@ public class BaseProtoLogImpl {
             mProtoLogEnabled = true;
             mProtoLogEnabledLockFree = true;
         }
-        sCacheUpdater.run();
     }
 
     /**
@@ -274,7 +259,6 @@ public class BaseProtoLogImpl {
                 throw new IllegalStateException("logging enabled while waiting for flush.");
             }
         }
-        sCacheUpdater.run();
     }
 
     /**
@@ -284,11 +268,11 @@ public class BaseProtoLogImpl {
         return mProtoLogEnabledLockFree;
     }
 
-    protected int setLogging(boolean setTextLogging, boolean value, PrintWriter pw,
+    private int setLogging(boolean setTextLogging, boolean value, ILogger logger,
             String... groups) {
         for (int i = 0; i < groups.length; i++) {
             String group = groups[i];
-            IProtoLogGroup g = LOG_GROUPS.get(group);
+            IProtoLogGroup g = mLogGroups.get(group);
             if (g != null) {
                 if (setTextLogging) {
                     g.setLogToLogcat(value);
@@ -296,11 +280,10 @@ public class BaseProtoLogImpl {
                     g.setLogToProto(value);
                 }
             } else {
-                logAndPrintln(pw, "No IProtoLogGroup named " + group);
+                logger.log("No IProtoLogGroup named " + group);
                 return -1;
             }
         }
-        sCacheUpdater.run();
         return 0;
     }
 
@@ -330,6 +313,7 @@ public class BaseProtoLogImpl {
         while ((arg = shell.getNextArg()) != null) {
             args.add(arg);
         }
+        final ILogger logger = (msg) -> logAndPrintln(pw, msg);
         String[] groups = args.toArray(new String[args.size()]);
         switch (cmd) {
             case "start":
@@ -342,14 +326,14 @@ public class BaseProtoLogImpl {
                 logAndPrintln(pw, getStatus());
                 return 0;
             case "enable":
-                return setLogging(false, true, pw, groups);
+                return setLogging(false, true, logger, groups);
             case "enable-text":
-                mViewerConfig.loadViewerConfig(pw, mViewerConfigFilename);
-                return setLogging(true, true, pw, groups);
+                mViewerConfig.loadViewerConfig(logger, mLegacyViewerConfigFilename);
+                return setLogging(true, true, logger, groups);
             case "disable":
-                return setLogging(false, false, pw, groups);
+                return setLogging(false, false, logger, groups);
             case "disable-text":
-                return setLogging(true, false, pw, groups);
+                return setLogging(true, false, logger, groups);
             default:
                 return unknownCommand(pw);
         }
@@ -362,12 +346,12 @@ public class BaseProtoLogImpl {
         return "ProtoLog status: "
                 + ((isProtoEnabled()) ? "Enabled" : "Disabled")
                 + "\nEnabled log groups: \n  Proto: "
-                + LOG_GROUPS.values().stream().filter(
-                    it -> it.isEnabled() && it.isLogToProto())
+                + mLogGroups.values().stream().filter(
+                        it -> it.isEnabled() && it.isLogToProto())
                 .map(IProtoLogGroup::name).collect(Collectors.joining(" "))
                 + "\n  Logcat: "
-                + LOG_GROUPS.values().stream().filter(
-                    it -> it.isEnabled() && it.isLogToLogcat())
+                + mLogGroups.values().stream().filter(
+                        it -> it.isEnabled() && it.isLogToLogcat())
                 .map(IProtoLogGroup::name).collect(Collectors.joining(" "))
                 + "\nLogging definitions loaded: " + mViewerConfig.knownViewerStringsNumber();
     }
@@ -393,5 +377,26 @@ public class BaseProtoLogImpl {
             pw.flush();
         }
     }
+
+    /**
+     * Start text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    public int startLoggingToLogcat(String[] groups, ILogger logger) {
+        mViewerConfig.loadViewerConfig(logger, mLegacyViewerConfigFilename);
+        return setLogging(true /* setTextLogging */, true, logger, groups);
+    }
+
+    /**
+     * Stop text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+        return setLogging(true /* setTextLogging */, false, logger, groups);
+    }
 }
 
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/LegacyProtoLogViewerConfigReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..1833410950bac1cef3b50a0bc5d37bdbbe998ba2
--- /dev/null
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogViewerConfigReader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 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;
+
+import com.android.internal.protolog.common.ILogger;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Handles loading and parsing of ProtoLog viewer configuration.
+ */
+public class LegacyProtoLogViewerConfigReader {
+
+    private static final String TAG = "ProtoLogViewerConfigReader";
+    private Map<Long, String> mLogMessageMap = null;
+
+    /** Returns message format string for its hash or null if unavailable. */
+    public synchronized String getViewerString(long messageHash) {
+        if (mLogMessageMap != null) {
+            return mLogMessageMap.get(messageHash);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Reads the specified viewer configuration file. Does nothing if the config is already loaded.
+     */
+    public synchronized void loadViewerConfig(ILogger logger, String viewerConfigFilename) {
+        try {
+            loadViewerConfig(new GZIPInputStream(new FileInputStream(viewerConfigFilename)));
+            logger.log("Loaded " + mLogMessageMap.size()
+                    + " log definitions from " + viewerConfigFilename);
+        } catch (FileNotFoundException e) {
+            logger.log("Unable to load log definitions: File "
+                    + viewerConfigFilename + " not found." + e);
+        } catch (IOException e) {
+            logger.log("Unable to load log definitions: IOException while reading "
+                    + viewerConfigFilename + ". " + e);
+        } catch (JSONException e) {
+            logger.log("Unable to load log definitions: JSON parsing exception while reading "
+                    + viewerConfigFilename + ". " + e);
+        }
+    }
+
+    /**
+     * Reads the specified viewer configuration input stream.
+     * Does nothing if the config is already loaded.
+     */
+    public synchronized void loadViewerConfig(InputStream viewerConfigInputStream)
+            throws IOException, JSONException {
+        if (mLogMessageMap != null) {
+            return;
+        }
+        InputStreamReader config = new InputStreamReader(viewerConfigInputStream);
+        BufferedReader reader = new BufferedReader(config);
+        StringBuilder builder = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            builder.append(line).append('\n');
+        }
+        reader.close();
+        JSONObject json = new JSONObject(builder.toString());
+        JSONObject messages = json.getJSONObject("messages");
+
+        mLogMessageMap = new TreeMap<>();
+        Iterator it = messages.keys();
+        while (it.hasNext()) {
+            String key = (String) it.next();
+            try {
+                long hash = Long.parseLong(key);
+                JSONObject val = messages.getJSONObject(key);
+                String msg = val.getString("message");
+                mLogMessageMap.put(hash, msg);
+            } catch (NumberFormatException expected) {
+                // Not a messageHash - skip it
+            }
+        }
+    }
+
+    /**
+     * Returns the number of loaded log definitions kept in memory.
+     */
+    public synchronized int knownViewerStringsNumber() {
+        if (mLogMessageMap != null) {
+            return mLogMessageMap.size();
+        }
+        return 0;
+    }
+
+}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..53062d837cfa55b26cbada04627f0d8730d7a521
--- /dev/null
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -0,0 +1,559 @@
+/*
+ * 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;
+
+import static perfetto.protos.PerfettoTrace.InternedData.PROTOLOG_STACKTRACE;
+import static perfetto.protos.PerfettoTrace.InternedData.PROTOLOG_STRING_ARGS;
+import static perfetto.protos.PerfettoTrace.InternedString.IID;
+import static perfetto.protos.PerfettoTrace.InternedString.STR;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.BOOLEAN_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.DOUBLE_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STACKTRACE_IID;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.MESSAGE_ID;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.SINT64_PARAMS;
+import static perfetto.protos.PerfettoTrace.ProtoLogMessage.STR_PARAM_IIDS;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.GROUPS;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.ID;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.NAME;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.Group.TAG;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MESSAGES;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.GROUP_ID;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.LEVEL;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE;
+import static perfetto.protos.PerfettoTrace.TracePacket.INTERNED_DATA;
+import static perfetto.protos.PerfettoTrace.TracePacket.PROTOLOG_MESSAGE;
+import static perfetto.protos.PerfettoTrace.TracePacket.PROTOLOG_VIEWER_CONFIG;
+import static perfetto.protos.PerfettoTrace.TracePacket.SEQUENCE_FLAGS;
+import static perfetto.protos.PerfettoTrace.TracePacket.SEQ_INCREMENTAL_STATE_CLEARED;
+import static perfetto.protos.PerfettoTrace.TracePacket.SEQ_NEEDS_INCREMENTAL_STATE;
+import static perfetto.protos.PerfettoTrace.TracePacket.TIMESTAMP;
+
+import android.annotation.Nullable;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.text.TextUtils;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.tracing.perfetto.TracingContext;
+import android.util.LongArray;
+import android.util.Slog;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogDataType;
+import com.android.internal.protolog.common.LogLevel;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData;
+
+/**
+ * A service for the ProtoLog logging system.
+ */
+public class PerfettoProtoLogImpl implements IProtoLog {
+    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
+    private static final String LOG_TAG = "ProtoLog";
+    private final AtomicInteger mTracingInstances = new AtomicInteger();
+
+    private final ProtoLogDataSource mDataSource = new ProtoLogDataSource(
+            this.mTracingInstances::incrementAndGet,
+            this::dumpTransitionTraceConfig,
+            this.mTracingInstances::decrementAndGet
+    );
+    private final ProtoLogViewerConfigReader mViewerConfigReader;
+    private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+
+    public PerfettoProtoLogImpl(String viewerConfigFilePath) {
+        this(() -> {
+            try {
+                return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
+            } catch (FileNotFoundException e) {
+                Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e);
+                return null;
+            }
+        });
+    }
+
+    public PerfettoProtoLogImpl(ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+        this(viewerConfigInputStreamProvider,
+                new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
+    }
+
+    @VisibleForTesting
+    public PerfettoProtoLogImpl(
+            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            ProtoLogViewerConfigReader viewerConfigReader
+    ) {
+        Producer.init(InitArguments.DEFAULTS);
+        mDataSource.register(DataSourceParams.DEFAULTS);
+        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
+        this.mViewerConfigReader = viewerConfigReader;
+    }
+
+    /**
+     * Main log method, do not call directly.
+     */
+    @VisibleForTesting
+    @Override
+    public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask,
+            @Nullable String messageString, Object[] args) {
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "log");
+
+        long tsNanos = SystemClock.elapsedRealtimeNanos();
+        try {
+            logToProto(level, group.name(), messageHash, paramsMask, args, tsNanos);
+            if (group.isLogToLogcat()) {
+                logToLogcat(group.getTag(), level, messageHash, messageString, args);
+            }
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
+    }
+
+    private void dumpTransitionTraceConfig() {
+        ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+
+        if (pis == null) {
+            Slog.w(LOG_TAG, "Failed to get viewer input stream.");
+            return;
+        }
+
+        mDataSource.trace(ctx -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            os.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos());
+
+            final long outProtologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG);
+            while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (pis.getFieldNumber() == (int) MESSAGES) {
+                    final long inMessageToken = pis.start(MESSAGES);
+                    final long outMessagesToken = os.start(MESSAGES);
+
+                    while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                        switch (pis.getFieldNumber()) {
+                            case (int) MessageData.MESSAGE_ID:
+                                os.write(MessageData.MESSAGE_ID,
+                                        pis.readLong(MessageData.MESSAGE_ID));
+                                break;
+                            case (int) MESSAGE:
+                                os.write(MESSAGE, pis.readString(MESSAGE));
+                                break;
+                            case (int) LEVEL:
+                                os.write(LEVEL, pis.readInt(LEVEL));
+                                break;
+                            case (int) GROUP_ID:
+                                os.write(GROUP_ID, pis.readInt(GROUP_ID));
+                                break;
+                            default:
+                                throw new RuntimeException(
+                                        "Unexpected field id " + pis.getFieldNumber());
+                        }
+                    }
+
+                    pis.end(inMessageToken);
+                    os.end(outMessagesToken);
+                }
+
+                if (pis.getFieldNumber() == (int) GROUPS) {
+                    final long inGroupToken = pis.start(GROUPS);
+                    final long outGroupToken = os.start(GROUPS);
+
+                    while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                        switch (pis.getFieldNumber()) {
+                            case (int) ID:
+                                int id = pis.readInt(ID);
+                                os.write(ID, id);
+                                break;
+                            case (int) NAME:
+                                String name = pis.readString(NAME);
+                                os.write(NAME, name);
+                                break;
+                            case (int) TAG:
+                                String tag = pis.readString(TAG);
+                                os.write(TAG, tag);
+                                break;
+                            default:
+                                throw new RuntimeException(
+                                        "Unexpected field id " + pis.getFieldNumber());
+                        }
+                    }
+
+                    pis.end(inGroupToken);
+                    os.end(outGroupToken);
+                }
+            }
+
+            os.end(outProtologViewerConfigToken);
+
+            ctx.flush();
+        });
+
+        mDataSource.flush();
+    }
+
+    private void logToLogcat(String tag, LogLevel level, long messageHash,
+            @Nullable String messageString, Object[] args) {
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToLogcat");
+        try {
+            doLogToLogcat(tag, level, messageHash, messageString, args);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
+    }
+
+    private void doLogToLogcat(String tag, LogLevel level, long messageHash,
+            @androidx.annotation.Nullable String messageString, Object[] args) {
+        String message = null;
+        if (messageString == null) {
+            messageString = mViewerConfigReader.getViewerString(messageHash);
+        }
+        if (messageString != null) {
+            if (args != null) {
+                try {
+                    message = TextUtils.formatSimple(messageString, args);
+                } catch (Exception ex) {
+                    Slog.w(LOG_TAG, "Invalid ProtoLog format string.", ex);
+                }
+            } else {
+                message = messageString;
+            }
+        }
+        if (message == null) {
+            StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE (" + messageHash + ")");
+            for (Object o : args) {
+                builder.append(" ").append(o);
+            }
+            message = builder.toString();
+        }
+        passToLogcat(tag, level, message);
+    }
+
+    /**
+     * SLog wrapper.
+     */
+    @VisibleForTesting
+    public void passToLogcat(String tag, LogLevel level, String message) {
+        switch (level) {
+            case DEBUG:
+                Slog.d(tag, message);
+                break;
+            case VERBOSE:
+                Slog.v(tag, message);
+                break;
+            case INFO:
+                Slog.i(tag, message);
+                break;
+            case WARN:
+                Slog.w(tag, message);
+                break;
+            case ERROR:
+                Slog.e(tag, message);
+                break;
+            case WTF:
+                Slog.wtf(tag, message);
+                break;
+        }
+    }
+
+    private void logToProto(LogLevel level, String groupName, long messageHash, int paramsMask,
+            Object[] args, long tsNanos) {
+        if (!isProtoEnabled()) {
+            return;
+        }
+
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToProto");
+        try {
+            doLogToProto(level, groupName, messageHash, paramsMask, args, tsNanos);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
+    }
+
+    private void doLogToProto(LogLevel level, String groupName, long messageHash, int paramsMask,
+            Object[] args, long tsNanos) {
+        mDataSource.trace(ctx -> {
+            final ProtoLogDataSource.TlsState tlsState = ctx.getCustomTlsState();
+            final LogLevel logFrom = tlsState.getLogFromLevel(groupName);
+
+            if (level.ordinal() < logFrom.ordinal()) {
+                return;
+            }
+
+            if (args != null) {
+                // Intern all string params before creating the trace packet for the proto
+                // message so that the interned strings appear before in the trace to make the
+                // trace processing easier.
+                int argIndex = 0;
+                for (Object o : args) {
+                    int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex);
+                    if (type == LogDataType.STRING) {
+                        internStringArg(ctx, o.toString());
+                    }
+                    argIndex++;
+                }
+            }
+
+            int internedStacktrace = 0;
+            if (tlsState.getShouldCollectStacktrace(groupName)) {
+                // Intern stackstraces before creating the trace packet for the proto message so
+                // that the interned stacktrace strings appear before in the trace to make the
+                // trace processing easier.
+                String stacktrace = collectStackTrace();
+                internedStacktrace = internStacktraceString(ctx, stacktrace);
+            }
+
+            final ProtoOutputStream os = ctx.newTracePacket();
+            os.write(TIMESTAMP, tsNanos);
+            long token = os.start(PROTOLOG_MESSAGE);
+            os.write(MESSAGE_ID, messageHash);
+
+            boolean needsIncrementalState = false;
+
+            if (args != null) {
+
+                int argIndex = 0;
+                LongArray longParams = new LongArray();
+                ArrayList<Double> doubleParams = new ArrayList<>();
+                ArrayList<Boolean> booleanParams = new ArrayList<>();
+                for (Object o : args) {
+                    int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex);
+                    try {
+                        switch (type) {
+                            case LogDataType.STRING:
+                                final int internedStringId = internStringArg(ctx, o.toString());
+                                os.write(STR_PARAM_IIDS, internedStringId);
+                                needsIncrementalState = true;
+                                break;
+                            case LogDataType.LONG:
+                                longParams.add(((Number) o).longValue());
+                                break;
+                            case LogDataType.DOUBLE:
+                                doubleParams.add(((Number) o).doubleValue());
+                                break;
+                            case LogDataType.BOOLEAN:
+                                booleanParams.add((boolean) o);
+                                break;
+                        }
+                    } catch (ClassCastException ex) {
+                        Slog.e(LOG_TAG, "Invalid ProtoLog paramsMask", ex);
+                    }
+                    argIndex++;
+                }
+
+                for (int i = 0; i < longParams.size(); ++i) {
+                    os.write(SINT64_PARAMS, longParams.get(i));
+                }
+                doubleParams.forEach(it -> os.write(DOUBLE_PARAMS, it));
+                // Converting booleans to int because Perfetto doesn't yet support repeated
+                // booleans, so we use a repeated integers instead (b/313651412).
+                booleanParams.forEach(it -> os.write(BOOLEAN_PARAMS, it ? 1 : 0));
+            }
+
+            if (tlsState.getShouldCollectStacktrace(groupName)) {
+                os.write(STACKTRACE_IID, internedStacktrace);
+            }
+
+            os.end(token);
+
+            if (needsIncrementalState) {
+                os.write(SEQUENCE_FLAGS, SEQ_NEEDS_INCREMENTAL_STATE);
+            }
+
+        });
+    }
+
+    private static final int STACK_SIZE_TO_PROTO_LOG_ENTRY_CALL = 12;
+
+    private String collectStackTrace() {
+        StackTraceElement[] stackTrace =  Thread.currentThread().getStackTrace();
+        StringWriter sw = new StringWriter();
+        try (PrintWriter pw = new PrintWriter(sw)) {
+            for (int i = STACK_SIZE_TO_PROTO_LOG_ENTRY_CALL; i < stackTrace.length; ++i) {
+                pw.println("\tat " + stackTrace[i]);
+            }
+        }
+
+        return sw.toString();
+    }
+
+    private int internStacktraceString(TracingContext<ProtoLogDataSource.Instance,
+            ProtoLogDataSource.TlsState,
+            ProtoLogDataSource.IncrementalState> ctx,
+            String stacktrace) {
+        final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
+        return internString(ctx, incrementalState.stacktraceInterningMap,
+                PROTOLOG_STACKTRACE, stacktrace);
+    }
+
+    private int internStringArg(
+            TracingContext<ProtoLogDataSource.Instance,
+            ProtoLogDataSource.TlsState,
+            ProtoLogDataSource.IncrementalState> ctx,
+            String string
+    ) {
+        final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
+        return internString(ctx, incrementalState.argumentInterningMap,
+                PROTOLOG_STRING_ARGS, string);
+    }
+
+    private int internString(
+            TracingContext<ProtoLogDataSource.Instance,
+                ProtoLogDataSource.TlsState,
+                ProtoLogDataSource.IncrementalState> ctx,
+            Map<String, Integer> internMap,
+            long fieldId,
+            String string
+    ) {
+        final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState();
+
+        if (!incrementalState.clearReported) {
+            final ProtoOutputStream os = ctx.newTracePacket();
+            os.write(SEQUENCE_FLAGS, SEQ_INCREMENTAL_STATE_CLEARED);
+            incrementalState.clearReported = true;
+        }
+
+        if (!internMap.containsKey(string)) {
+            final int internedIndex = internMap.size() + 1;
+            internMap.put(string, internedIndex);
+
+            final ProtoOutputStream os = ctx.newTracePacket();
+            final long token = os.start(INTERNED_DATA);
+            final long innerToken = os.start(fieldId);
+            os.write(IID, internedIndex);
+            os.write(STR, string.getBytes());
+            os.end(innerToken);
+            os.end(token);
+        }
+
+        return internMap.get(string);
+    }
+
+    /**
+     * Responds to a shell command.
+     */
+    public int onShellCommand(ShellCommand shell) {
+        PrintWriter pw = shell.getOutPrintWriter();
+        String cmd = shell.getNextArg();
+        if (cmd == null) {
+            return unknownCommand(pw);
+        }
+        ArrayList<String> args = new ArrayList<>();
+        String arg;
+        while ((arg = shell.getNextArg()) != null) {
+            args.add(arg);
+        }
+        final ILogger logger = (msg) -> logAndPrintln(pw, msg);
+        String[] groups = args.toArray(new String[args.size()]);
+        switch (cmd) {
+            case "enable-text":
+                return this.startLoggingToLogcat(groups, logger);
+            case "disable-text":
+                return this.stopLoggingToLogcat(groups, logger);
+            default:
+                return unknownCommand(pw);
+        }
+    }
+
+    private int unknownCommand(PrintWriter pw) {
+        pw.println("Unknown command");
+        pw.println("Window manager logging options:");
+        pw.println("  enable-text [group...]: Enable logcat logging for given groups");
+        pw.println("  disable-text [group...]: Disable logcat logging for given groups");
+        return -1;
+    }
+
+    /**
+     * Returns {@code true} iff logging to proto is enabled.
+     */
+    public boolean isProtoEnabled() {
+        return mTracingInstances.get() > 0;
+    }
+
+    /**
+     * Start text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    public int startLoggingToLogcat(String[] groups, ILogger logger) {
+        mViewerConfigReader.loadViewerConfig(logger);
+        return setTextLogging(true, logger, groups);
+    }
+
+    /**
+     * Stop text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+        mViewerConfigReader.unloadViewerConfig();
+        return setTextLogging(false, logger, groups);
+    }
+
+    /**
+     * Start logging the stack trace of the when the log message happened for target groups
+     * @return status code
+     */
+    public int startLoggingStackTrace(String[] groups, ILogger logger) {
+        return -1;
+    }
+
+    /**
+     * Stop logging the stack trace of the when the log message happened for target groups
+     * @return status code
+     */
+    public int stopLoggingStackTrace() {
+        return -1;
+    }
+
+    private int setTextLogging(boolean value, ILogger logger, String... groups) {
+        for (int i = 0; i < groups.length; i++) {
+            String group = groups[i];
+            IProtoLogGroup g = mLogGroups.get(group);
+            if (g != null) {
+                g.setLogToLogcat(value);
+            } else {
+                logger.log("No IProtoLogGroup named " + group);
+                return -1;
+            }
+        }
+        return 0;
+    }
+
+    static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
+        Slog.i(LOG_TAG, msg);
+        if (pw != null) {
+            pw.println(msg);
+            pw.flush();
+        }
+    }
+}
+
diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..a8ff75d6595e60d4931df31aa8366fe05bbb9c73
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2023 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;
+
+import static perfetto.protos.PerfettoTrace.DataSourceConfig.PROTOLOG_CONFIG;
+import static perfetto.protos.PerfettoTrace.ProtoLogConfig.GROUP_OVERRIDES;
+import static perfetto.protos.PerfettoTrace.ProtoLogConfig.TRACING_MODE;
+import static perfetto.protos.PerfettoTrace.ProtoLogGroup.COLLECT_STACKTRACE;
+import static perfetto.protos.PerfettoTrace.ProtoLogGroup.LOG_FROM;
+import static perfetto.protos.PerfettoTrace.ProtoLogGroup.GROUP_NAME;
+
+import android.tracing.perfetto.CreateIncrementalStateArgs;
+import android.tracing.perfetto.CreateTlsStateArgs;
+import android.tracing.perfetto.DataSource;
+import android.tracing.perfetto.DataSourceInstance;
+import android.tracing.perfetto.FlushCallbackArguments;
+import android.tracing.perfetto.StartCallbackArguments;
+import android.tracing.perfetto.StopCallbackArguments;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.WireTypeMismatchException;
+
+import com.android.internal.protolog.common.LogLevel;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import perfetto.protos.PerfettoTrace;
+
+public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance,
+        ProtoLogDataSource.TlsState,
+        ProtoLogDataSource.IncrementalState> {
+
+    private final Runnable mOnStart;
+    private final Runnable mOnFlush;
+    private final Runnable mOnStop;
+
+    public ProtoLogDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) {
+        super("android.protolog");
+        this.mOnStart = onStart;
+        this.mOnFlush = onFlush;
+        this.mOnStop = onStop;
+    }
+
+    @Override
+    public Instance createInstance(ProtoInputStream configStream, int instanceIndex) {
+        ProtoLogConfig config = null;
+
+        try {
+            while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                try {
+                    if (configStream.getFieldNumber() == (int) PROTOLOG_CONFIG) {
+                        if (config != null) {
+                            throw new RuntimeException("ProtoLog config already set in loop");
+                        }
+                        config = readProtoLogConfig(configStream);
+                    }
+                } catch (WireTypeMismatchException e) {
+                    throw new RuntimeException("Failed to parse ProtoLog DataSource config", e);
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to read ProtoLog DataSource config", e);
+        }
+
+        if (config == null) {
+            // No config found
+            config = ProtoLogConfig.DEFAULT;
+        }
+
+        return new Instance(
+                this, instanceIndex, config, mOnStart, mOnFlush, mOnStop);
+    }
+
+    @Override
+    public TlsState createTlsState(CreateTlsStateArgs<Instance> args) {
+        try (Instance dsInstance = args.getDataSourceInstanceLocked()) {
+            if (dsInstance == null) {
+                // Datasource instance has been removed
+                return new TlsState(ProtoLogConfig.DEFAULT);
+            }
+            return new TlsState(dsInstance.mConfig);
+        }
+    }
+
+    @Override
+    public IncrementalState createIncrementalState(CreateIncrementalStateArgs<Instance> args) {
+        return new IncrementalState();
+    }
+
+    public static class TlsState {
+        private final ProtoLogConfig mConfig;
+
+        private TlsState(ProtoLogConfig config) {
+            this.mConfig = config;
+        }
+
+        /**
+         * Get the log from level for a group.
+         * @param groupTag The tag of the group to get the log from level.
+         * @return The lowest LogLevel (inclusive) to log message from.
+         */
+        public LogLevel getLogFromLevel(String groupTag) {
+            return getConfigFor(groupTag).logFrom;
+        }
+
+        /**
+         * Get if the stacktrace for the log message should be collected for this group.
+         * @param groupTag The tag of the group to get whether or not a stacktrace was requested.
+         * @return True iff a stacktrace was requested to be collected from this group in the
+         *         tracing config.
+         */
+        public boolean getShouldCollectStacktrace(String groupTag) {
+            return getConfigFor(groupTag).collectStackTrace;
+        }
+
+        private GroupConfig getConfigFor(String groupTag) {
+            return mConfig.getConfigFor(groupTag);
+        }
+    }
+
+    public static class IncrementalState {
+        public final Map<String, Integer> argumentInterningMap = new HashMap<>();
+        public final Map<String, Integer> stacktraceInterningMap = new HashMap<>();
+        public boolean clearReported = false;
+    }
+
+    private static class ProtoLogConfig {
+        private final LogLevel mDefaultLogFromLevel;
+        private final Map<String, GroupConfig> mGroupConfigs;
+
+        private static final ProtoLogConfig DEFAULT =
+                new ProtoLogConfig(LogLevel.WTF, new HashMap<>());
+
+        private ProtoLogConfig(
+                LogLevel defaultLogFromLevel, Map<String, GroupConfig> groupConfigs) {
+            this.mDefaultLogFromLevel = defaultLogFromLevel;
+            this.mGroupConfigs = groupConfigs;
+        }
+
+        private GroupConfig getConfigFor(String groupTag) {
+            return mGroupConfigs.getOrDefault(groupTag, getDefaultGroupConfig());
+        }
+
+        private GroupConfig getDefaultGroupConfig() {
+            return new GroupConfig(mDefaultLogFromLevel, false);
+        }
+    }
+
+    public static class GroupConfig {
+        public final LogLevel logFrom;
+        public final boolean collectStackTrace;
+
+        public GroupConfig(LogLevel logFromLevel, boolean collectStackTrace) {
+            this.logFrom = logFromLevel;
+            this.collectStackTrace = collectStackTrace;
+        }
+    }
+
+    private ProtoLogConfig readProtoLogConfig(ProtoInputStream configStream)
+            throws IOException {
+        final long config_token = configStream.start(PROTOLOG_CONFIG);
+
+        LogLevel defaultLogFromLevel = LogLevel.WTF;
+        final Map<String, GroupConfig> groupConfigs = new HashMap<>();
+
+        while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            if (configStream.getFieldNumber() == (int) TRACING_MODE) {
+                int tracingMode = configStream.readInt(TRACING_MODE);
+                switch (tracingMode) {
+                    case PerfettoTrace.ProtoLogConfig.DEFAULT:
+                        break;
+                    case PerfettoTrace.ProtoLogConfig.ENABLE_ALL:
+                        defaultLogFromLevel = LogLevel.DEBUG;
+                        break;
+                    default:
+                        throw new RuntimeException("Unhandled ProtoLog tracing mode type");
+                }
+            }
+            if (configStream.getFieldNumber() == (int) GROUP_OVERRIDES) {
+                final long group_overrides_token  = configStream.start(GROUP_OVERRIDES);
+
+                String tag = null;
+                LogLevel logFromLevel = defaultLogFromLevel;
+                boolean collectStackTrace = false;
+                while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    if (configStream.getFieldNumber() == (int) GROUP_NAME) {
+                        tag = configStream.readString(GROUP_NAME);
+                    }
+                    if (configStream.getFieldNumber() == (int) LOG_FROM) {
+                        final int logFromInt = configStream.readInt(LOG_FROM);
+                        switch (logFromInt) {
+                            case (PerfettoTrace.PROTOLOG_LEVEL_DEBUG): {
+                                logFromLevel = LogLevel.DEBUG;
+                                break;
+                            }
+                            case (PerfettoTrace.PROTOLOG_LEVEL_VERBOSE): {
+                                logFromLevel = LogLevel.VERBOSE;
+                                break;
+                            }
+                            case (PerfettoTrace.PROTOLOG_LEVEL_INFO): {
+                                logFromLevel = LogLevel.INFO;
+                                break;
+                            }
+                            case (PerfettoTrace.PROTOLOG_LEVEL_WARN): {
+                                logFromLevel = LogLevel.WARN;
+                                break;
+                            }
+                            case (PerfettoTrace.PROTOLOG_LEVEL_ERROR): {
+                                logFromLevel = LogLevel.ERROR;
+                                break;
+                            }
+                            case (PerfettoTrace.PROTOLOG_LEVEL_WTF): {
+                                logFromLevel = LogLevel.WTF;
+                                break;
+                            }
+                            default: {
+                                throw new RuntimeException("Unhandled log level");
+                            }
+                        }
+                    }
+                    if (configStream.getFieldNumber() == (int) COLLECT_STACKTRACE) {
+                        collectStackTrace = configStream.readBoolean(COLLECT_STACKTRACE);
+                    }
+                }
+
+                if (tag == null) {
+                    throw new RuntimeException("Failed to decode proto config. "
+                            + "Got a group override without a group tag.");
+                }
+
+                groupConfigs.put(tag, new GroupConfig(logFromLevel, collectStackTrace));
+
+                configStream.end(group_overrides_token);
+            }
+        }
+
+        configStream.end(config_token);
+
+        return new ProtoLogConfig(defaultLogFromLevel, groupConfigs);
+    }
+
+    public static class Instance extends DataSourceInstance {
+
+        private final Runnable mOnStart;
+        private final Runnable mOnFlush;
+        private final Runnable mOnStop;
+        private final ProtoLogConfig mConfig;
+
+        public Instance(
+                DataSource<Instance, TlsState, IncrementalState> dataSource,
+                int instanceIdx,
+                ProtoLogConfig config,
+                Runnable onStart,
+                Runnable onFlush,
+                Runnable onStop
+        ) {
+            super(dataSource, instanceIdx);
+            this.mOnStart = onStart;
+            this.mOnFlush = onFlush;
+            this.mOnStop = onStop;
+            this.mConfig = config;
+        }
+
+        @Override
+        public void onStart(StartCallbackArguments args) {
+            this.mOnStart.run();
+        }
+
+        @Override
+        public void onFlush(FlushCallbackArguments args) {
+            this.mOnFlush.run();
+        }
+
+        @Override
+        public void onStop(StopCallbackArguments args) {
+            this.mOnStop.run();
+        }
+    }
+}
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 527cfddf6d8e0b1214d07210a111a9c881738379..78bed9478d841efdb80d0c1148126b7da8f53c63 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -16,30 +16,35 @@
 
 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 android.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
-
-import java.io.File;
+import com.android.internal.protolog.common.LogLevel;
+import com.android.internal.protolog.common.ProtoLogToolInjected;
 
 /**
  * A service for the ProtoLog logging system.
  */
-public class ProtoLogImpl extends BaseProtoLogImpl {
-    private static final int BUFFER_CAPACITY = 1024 * 1024;
-    private static final String LOG_FILENAME = "/data/misc/wmtrace/wm_log.winscope";
-    private static final String VIEWER_CONFIG_FILENAME = "/system/etc/protolog.conf.json.gz";
-    private static final int PER_CHUNK_SIZE = 1024;
+public class ProtoLogImpl {
+    private static IProtoLog sServiceInstance = null;
 
-    private static ProtoLogImpl sServiceInstance = null;
+    @ProtoLogToolInjected(VIEWER_CONFIG_PATH)
+    private static String sViewerConfigPath;
 
-    static {
-        addLogGroupEnum(ProtoLogGroup.values());
-    }
+    @ProtoLogToolInjected(LEGACY_VIEWER_CONFIG_PATH)
+    private static String sLegacyViewerConfigPath;
+
+    @ProtoLogToolInjected(LEGACY_OUTPUT_FILE_PATH)
+    private static String sLegacyOutputFilePath;
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void d(IProtoLogGroup group, int messageHash, int paramsMask,
+    public static void d(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
             Object... args) {
         getSingleInstance()
@@ -47,7 +52,7 @@ public class ProtoLogImpl extends BaseProtoLogImpl {
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void v(IProtoLogGroup group, int messageHash, int paramsMask,
+    public static void v(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
             Object... args) {
         getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString,
@@ -55,21 +60,21 @@ public class ProtoLogImpl extends BaseProtoLogImpl {
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void i(IProtoLogGroup group, int messageHash, int paramsMask,
+    public static void i(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
             Object... args) {
         getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void w(IProtoLogGroup group, int messageHash, int paramsMask,
+    public static void w(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
             Object... args) {
         getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args);
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void e(IProtoLogGroup group, int messageHash, int paramsMask,
+    public static void e(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
             Object... args) {
         getSingleInstance()
@@ -77,40 +82,36 @@ public class ProtoLogImpl extends BaseProtoLogImpl {
     }
 
     /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void wtf(IProtoLogGroup group, int messageHash, int paramsMask,
+    public static void wtf(IProtoLogGroup group, long messageHash, int paramsMask,
             @Nullable String messageString,
             Object... args) {
         getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args);
     }
 
-    /** Returns true iff logging is enabled for the given {@code IProtoLogGroup}. */
     public static boolean isEnabled(IProtoLogGroup group) {
-        return group.isLogToLogcat()
-                || (group.isLogToProto() && getSingleInstance().isProtoEnabled());
+        // TODO: Implement for performance reasons, with optional level parameter?
+        return true;
     }
 
     /**
      * Returns the single instance of the ProtoLogImpl singleton class.
      */
-    public static synchronized ProtoLogImpl getSingleInstance() {
+    public static synchronized IProtoLog getSingleInstance() {
         if (sServiceInstance == null) {
-            sServiceInstance = new ProtoLogImpl(
-                    new File(LOG_FILENAME)
-                    , BUFFER_CAPACITY
-                    , new ProtoLogViewerConfigReader()
-                    , PER_CHUNK_SIZE);
+            if (android.tracing.Flags.perfettoProtolog()) {
+                sServiceInstance =
+                        new PerfettoProtoLogImpl(sViewerConfigPath);
+            } else {
+                sServiceInstance =
+                        new LegacyProtoLogImpl(sLegacyOutputFilePath, sLegacyViewerConfigPath);
+            }
         }
         return sServiceInstance;
     }
 
     @VisibleForTesting
-    public static synchronized void setSingleInstance(@Nullable ProtoLogImpl instance) {
+    public static synchronized void setSingleInstance(@Nullable IProtoLog instance) {
         sServiceInstance = instance;
     }
-
-    public ProtoLogImpl(File logFile, int bufferCapacity,
-            ProtoLogViewerConfigReader viewConfigReader, int perChunkSize) {
-        super(logFile, VIEWER_CONFIG_FILENAME, bufferCapacity, viewConfigReader, perChunkSize);
-  }
 }
 
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index aa30a7723ad973537aa9fb22d95ec0fc9dfdca6f..3c206acf7c0ff496519359f8807c6f35845e8b20 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -1,125 +1,93 @@
-/*
- * Copyright (C) 2020 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;
 
-import android.annotation.Nullable;
-import android.util.Slog;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MESSAGES;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE;
+import static perfetto.protos.PerfettoTrace.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
+
+import android.util.proto.ProtoInputStream;
 
-import org.json.JSONException;
-import org.json.JSONObject;
+import com.android.internal.protolog.common.ILogger;
 
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.util.Iterator;
 import java.util.Map;
-import java.util.TreeMap;
-import java.util.zip.GZIPInputStream;
 
-/**
- * Handles loading and parsing of ProtoLog viewer configuration.
- */
 public class ProtoLogViewerConfigReader {
-    private static final String TAG = "ProtoLogViewerConfigReader";
-    private Map<Integer, String> mLogMessageMap = null;
+    private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+    private Map<Long, String> mLogMessageMap = null;
 
-    /** Returns message format string for its hash or null if unavailable. */
-    public synchronized String getViewerString(int messageHash) {
-        if (mLogMessageMap != null) {
-            return mLogMessageMap.get(messageHash);
-        } else {
-            return null;
-        }
+    public ProtoLogViewerConfigReader(
+            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
     }
 
     /**
-     * Reads the specified viewer configuration file. Does nothing if the config is already loaded.
+     * Returns message format string for its hash or null if unavailable
+     * or the viewer config is not loaded into memory.
      */
-    public synchronized void loadViewerConfig(PrintWriter pw, String viewerConfigFilename) {
-        try {
-            loadViewerConfig(new GZIPInputStream(new FileInputStream(viewerConfigFilename)));
-            logAndPrintln(pw, "Loaded " + mLogMessageMap.size()
-                    + " log definitions from " + viewerConfigFilename);
-        } catch (FileNotFoundException e) {
-            logAndPrintln(pw, "Unable to load log definitions: File "
-                    + viewerConfigFilename + " not found." + e);
-        } catch (IOException e) {
-            logAndPrintln(pw, "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 "
-                    + viewerConfigFilename + ". " + e);
+    public synchronized String getViewerString(long messageHash) {
+        if (mLogMessageMap != null) {
+            return mLogMessageMap.get(messageHash);
+        } else {
+            return null;
         }
     }
 
     /**
-     * Reads the specified viewer configuration input stream.
-     * Does nothing if the config is already loaded.
+     * Loads the viewer config into memory. No-op if already loaded in memory.
      */
-    public synchronized void loadViewerConfig(InputStream viewerConfigInputStream)
-            throws IOException, JSONException {
+    public synchronized void loadViewerConfig(ILogger logger) {
         if (mLogMessageMap != null) {
             return;
         }
-        InputStreamReader config = new InputStreamReader(viewerConfigInputStream);
-        BufferedReader reader = new BufferedReader(config);
-        StringBuilder builder = new StringBuilder();
-        String line;
-        while ((line = reader.readLine()) != null) {
-            builder.append(line).append('\n');
-        }
-        reader.close();
-        JSONObject json = new JSONObject(builder.toString());
-        JSONObject messages = json.getJSONObject("messages");
-
-        mLogMessageMap = new TreeMap<>();
-        Iterator it = messages.keys();
-        while (it.hasNext()) {
-            String key = (String) it.next();
-            try {
-                int hash = Integer.parseInt(key);
-                JSONObject val = messages.getJSONObject(key);
-                String msg = val.getString("message");
-                mLogMessageMap.put(hash, msg);
-            } catch (NumberFormatException expected) {
-                // Not a messageHash - skip it
-            }
+
+        try {
+            doLoadViewerConfig();
+            logger.log("Loaded " + mLogMessageMap.size() + " log definitions");
+        } catch (IOException e) {
+            logger.log("Unable to load log definitions: "
+                    + "IOException while processing viewer config" + e);
         }
     }
 
     /**
-     * Returns the number of loaded log definitions kept in memory.
+     * Unload the viewer config from memory.
      */
-    public synchronized int knownViewerStringsNumber() {
-        if (mLogMessageMap != null) {
-            return mLogMessageMap.size();
-        }
-        return 0;
+    public synchronized void unloadViewerConfig() {
+        mLogMessageMap = null;
     }
 
-    static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
-        Slog.i(TAG, msg);
-        if (pw != null) {
-            pw.println(msg);
-            pw.flush();
+    private void doLoadViewerConfig() throws IOException {
+        final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+
+        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            if (pis.getFieldNumber() == (int) MESSAGES) {
+                final long inMessageToken = pis.start(MESSAGES);
+
+                long messageId = 0;
+                String message = null;
+                while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (pis.getFieldNumber()) {
+                        case (int) MESSAGE_ID:
+                            messageId = pis.readLong(MESSAGE_ID);
+                            break;
+                        case (int) MESSAGE:
+                            message = pis.readString(MESSAGE);
+                            break;
+                    }
+                }
+
+                if (messageId == 0) {
+                    throw new IOException("Failed to get message id");
+                }
+
+                if (message == null) {
+                    throw new IOException("Failed to get message string");
+                }
+
+                mLogMessageMap.put(messageId, message);
+
+                pis.end(inMessageToken);
+            }
         }
     }
 }
diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..334f5488425ac0463d3bdc3dce52ab3cd12d9edb
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+import android.util.proto.ProtoInputStream;
+
+public interface ViewerConfigInputStreamProvider {
+    /**
+     * @return a ProtoInputStream.
+     */
+    ProtoInputStream getInputStream();
+}
diff --git a/core/java/com/android/internal/protolog/common/ILogger.java b/core/java/com/android/internal/protolog/common/ILogger.java
new file mode 100644
index 0000000000000000000000000000000000000000..cc6fa5e73969f08f271d79be2ddbb1e1d31ab9da
--- /dev/null
+++ b/core/java/com/android/internal/protolog/common/ILogger.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+public interface ILogger {
+    /**
+     * Log a message.
+     * @param message The log message.
+     */
+    void log(String message);
+}
diff --git a/core/java/com/android/internal/protolog/common/IProtoLog.java b/core/java/com/android/internal/protolog/common/IProtoLog.java
new file mode 100644
index 0000000000000000000000000000000000000000..c06d14b2075eb38c6b7c6b8f6f13c2de79971327
--- /dev/null
+++ b/core/java/com/android/internal/protolog/common/IProtoLog.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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;
+
+/**
+ * Interface for ProtoLog implementations.
+ */
+public interface IProtoLog {
+
+    /**
+     * Log a ProtoLog message
+     * @param logLevel Log level of the proto message.
+     * @param group The group this message belongs to.
+     * @param messageHash The hash of the message.
+     * @param paramsMask The parameters mask of the message.
+     * @param messageString The message string.
+     * @param args The arguments of the message.
+     */
+    void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
+             String messageString, Object[] args);
+
+    /**
+     * Check if ProtoLog is tracing.
+     * @return true iff a ProtoLog tracing session is active.
+     */
+    boolean isProtoEnabled();
+
+    /**
+     * Start logging log groups to logcat
+     * @param groups Groups to start text logging for
+     * @return status code
+     */
+    int startLoggingToLogcat(String[] groups, ILogger logger);
+
+    /**
+     * Stop logging log groups to logcat
+     * @param groups Groups to start text logging for
+     * @return status code
+     */
+    int stopLoggingToLogcat(String[] groups, ILogger logger);
+}
diff --git a/core/java/com/android/internal/protolog/common/IProtoLogGroup.java b/core/java/com/android/internal/protolog/common/IProtoLogGroup.java
index e3db46832a6f575d26f5eb89f314921e09f3fcdb..4e9686f99f4b6de7b99987d6ccd936a21270ebc9 100644
--- a/core/java/com/android/internal/protolog/common/IProtoLogGroup.java
+++ b/core/java/com/android/internal/protolog/common/IProtoLogGroup.java
@@ -26,6 +26,7 @@ public interface IProtoLogGroup {
     boolean isEnabled();
 
     /**
+     * @deprecated TODO(b/324128613) remove once we migrate fully to Perfetto
      * is binary logging enabled for the group.
      */
     boolean isLogToProto();
diff --git a/core/java/com/android/internal/protolog/common/ProtoLog.java b/core/java/com/android/internal/protolog/common/ProtoLog.java
index 8870096f3db7fa4fdd90a72e70e799661cab47f0..18e3f66c479516b98ae6edc0d6be43c700bf90c2 100644
--- a/core/java/com/android/internal/protolog/common/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/common/ProtoLog.java
@@ -16,8 +16,6 @@
 
 package com.android.internal.protolog.common;
 
-import android.util.Log;
-
 /**
  * ProtoLog API - exposes static logging methods. Usage of this API is similar
  * to {@code android.utils.Log} class. Instead of plain text log messages each call consists of
@@ -55,9 +53,6 @@ public class ProtoLog {
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
-        if (group.isLogToLogcat()) {
-            Log.d(group.getTag(), String.format(messageString, args));
-        }
     }
 
     /**
@@ -73,9 +68,6 @@ public class ProtoLog {
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
-        if (group.isLogToLogcat()) {
-            Log.v(group.getTag(), String.format(messageString, args));
-        }
     }
 
     /**
@@ -91,9 +83,6 @@ public class ProtoLog {
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
-        if (group.isLogToLogcat()) {
-            Log.i(group.getTag(), String.format(messageString, args));
-        }
     }
 
     /**
@@ -109,9 +98,6 @@ public class ProtoLog {
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
-        if (group.isLogToLogcat()) {
-            Log.w(group.getTag(), String.format(messageString, args));
-        }
     }
 
     /**
@@ -127,9 +113,6 @@ public class ProtoLog {
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
-        if (group.isLogToLogcat()) {
-            Log.e(group.getTag(), String.format(messageString, args));
-        }
     }
 
     /**
@@ -145,8 +128,30 @@ public class ProtoLog {
             throw new UnsupportedOperationException(
                     "ProtoLog calls MUST be processed with ProtoLogTool");
         }
-        if (group.isLogToLogcat()) {
-            Log.wtf(group.getTag(), String.format(messageString, args));
+    }
+
+    /**
+     * Check if ProtoLog isEnabled for a target group.
+     * @param group Group to check enable status of.
+     * @return true iff this is being logged.
+     */
+    public static boolean isEnabled(IProtoLogGroup group) {
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
+        }
+        return false;
+    }
+
+    /**
+     * Get the single ProtoLog instance.
+     * @return A singleton instance of ProtoLog.
+     */
+    public static IProtoLog getSingleInstance() {
+        if (REQUIRE_PROTOLOGTOOL) {
+            throw new UnsupportedOperationException(
+                    "ProtoLog calls MUST be processed with ProtoLogTool");
         }
+        return null;
     }
 }
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 0000000000000000000000000000000000000000..ffd0d762385278feec71cda18aca817f2104c894
--- /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/core/proto/android/internal/protolog.proto b/core/proto/android/internal/protolog.proto
index fee7a878f860dfcc5dc1827e8112688df1392255..9e205e284c53c6bdbc4207e9a93a3e5cff8fe15f 100644
--- a/core/proto/android/internal/protolog.proto
+++ b/core/proto/android/internal/protolog.proto
@@ -27,7 +27,7 @@ message ProtoLogMessage {
     option (.android.msg_privacy).dest = DEST_LOCAL;
 
     /* log statement identifier, created from message string and log level. */
-    optional sfixed32 message_hash = 1;
+    optional sfixed32 message_hash_legacy = 1 [deprecated = true];
     /* log time, relative to the elapsed system time clock. */
     optional fixed64 elapsed_realtime_nanos = 2;
     /* string parameters passed to the log call. */
@@ -38,6 +38,9 @@ message ProtoLogMessage {
     repeated double double_params = 5 [packed=true];
     /* boolean parameters passed to the log call. */
     repeated bool boolean_params = 6 [packed=true];
+
+    /* log statement identifier, created from message string and log level. */
+    optional sfixed64 message_hash = 7;
 }
 
 /* represents a log file containing ProtoLog log entries.
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index 1fd10031a129d0687431fd83db6b4010c85a378f..238a3e10f058d36c1843a2024de6c30a4b752157 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -199,3 +199,8 @@ filegroup {
     name: "services.core.protolog.json",
     srcs: ["services.core.protolog.json"],
 }
+
+filegroup {
+    name: "file-core.protolog.pb",
+    srcs: ["core.protolog.pb"],
+}
diff --git a/data/etc/core.protolog.pb b/data/etc/core.protolog.pb
new file mode 100644
index 0000000000000000000000000000000000000000..0415e44af72a6e2197c9f6fd54cabd6392d795df
Binary files /dev/null and b/data/etc/core.protolog.pb differ
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index d66c925de376978a2b9e5c1d4b2939969804a797..0ecf1f8f1feb50e1f741b86ae365404e503d8900 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -82,16 +82,18 @@ filegroup {
 genrule {
     name: "wm_shell_protolog_src",
     srcs: [
+        ":protolog-impl",
         ":wm_shell_protolog-groups",
         ":wm_shell-sources",
     ],
     tools: ["protologtool"],
     cmd: "$(location protologtool) transform-protolog-calls " +
         "--protolog-class com.android.internal.protolog.common.ProtoLog " +
-        "--protolog-impl-class com.android.wm.shell.protolog.ShellProtoLogImpl " +
-        "--protolog-cache-class com.android.wm.shell.protolog.ShellProtoLogCache " +
         "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
         "--loggroups-jar $(location :wm_shell_protolog-groups) " +
+        "--viewer-config-file-path /system_ext/etc/wmshell.protolog.pb " +
+        "--legacy-viewer-config-file-path /system_ext/etc/wmshell.protolog.json.gz " +
+        "--legacy-output-file-path /data/misc/wmtrace/shell_log.winscope " +
         "--output-srcjar $(out) " +
         "$(locations :wm_shell-sources)",
     out: ["wm_shell_protolog.srcjar"],
@@ -108,11 +110,29 @@ genrule {
         "--protolog-class com.android.internal.protolog.common.ProtoLog " +
         "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
         "--loggroups-jar $(location :wm_shell_protolog-groups) " +
-        "--viewer-conf $(out) " +
+        "--viewer-config-type json " +
+        "--viewer-config $(out) " +
         "$(locations :wm_shell-sources)",
     out: ["wm_shell_protolog.json"],
 }
 
+genrule {
+    name: "gen-wmshell.protolog.pb",
+    srcs: [
+        ":wm_shell_protolog-groups",
+        ":wm_shell-sources",
+    ],
+    tools: ["protologtool"],
+    cmd: "$(location protologtool) generate-viewer-config " +
+        "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+        "--loggroups-class com.android.wm.shell.protolog.ShellProtoLogGroup " +
+        "--loggroups-jar $(location :wm_shell_protolog-groups) " +
+        "--viewer-config-type proto " +
+        "--viewer-config $(out) " +
+        "$(locations :wm_shell-sources)",
+    out: ["wmshell.protolog.pb"],
+}
+
 genrule {
     name: "protolog.json.gz",
     srcs: [":generate-wm_shell_protolog.json"],
@@ -127,6 +147,13 @@ prebuilt_etc {
     filename_from_src: true,
 }
 
+prebuilt_etc {
+    name: "wmshell.protolog.pb",
+    system_ext_specific: true,
+    src: ":gen-wmshell.protolog.pb",
+    filename_from_src: true,
+}
+
 // End ProtoLog
 
 java_library {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
index 88525aabe53b3f7e1f2e8c2cb01ddb0d5f977fd8..93893e33d2d5991c5e49be306fafd6451661fc5a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
@@ -16,7 +16,10 @@
 
 package com.android.wm.shell;
 
-import com.android.wm.shell.protolog.ShellProtoLogImpl;
+import com.android.internal.protolog.LegacyProtoLogImpl;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -24,19 +27,19 @@ import java.io.PrintWriter;
 import java.util.Arrays;
 
 /**
- * Controls the {@link ShellProtoLogImpl} in WMShell via adb shell commands.
+ * Controls the {@link ProtoLog} in WMShell via adb shell commands.
  *
  * Use with {@code adb shell dumpsys activity service SystemUIService WMShell protolog ...}.
  */
 public class ProtoLogController implements ShellCommandHandler.ShellCommandActionHandler {
     private final ShellCommandHandler mShellCommandHandler;
-    private final ShellProtoLogImpl mShellProtoLog;
+    private final IProtoLog mShellProtoLog;
 
     public ProtoLogController(ShellInit shellInit,
             ShellCommandHandler shellCommandHandler) {
         shellInit.addInitCallback(this::onInit, this);
         mShellCommandHandler = shellCommandHandler;
-        mShellProtoLog = ShellProtoLogImpl.getSingleInstance();
+        mShellProtoLog = ProtoLog.getSingleInstance();
     }
 
     void onInit() {
@@ -45,22 +48,35 @@ public class ProtoLogController implements ShellCommandHandler.ShellCommandActio
 
     @Override
     public boolean onShellCommand(String[] args, PrintWriter pw) {
+        final ILogger logger = pw::println;
         switch (args[0]) {
             case "status": {
-                pw.println(mShellProtoLog.getStatus());
+                if (android.tracing.Flags.perfettoProtolog()) {
+                    pw.println("(Deprecated) legacy command. Use Perfetto commands instead.");
+                    return false;
+                }
+                ((LegacyProtoLogImpl) mShellProtoLog).getStatus();
                 return true;
             }
             case "start": {
-                mShellProtoLog.startProtoLog(pw);
+                if (android.tracing.Flags.perfettoProtolog()) {
+                    pw.println("(Deprecated) legacy command. Use Perfetto commands instead.");
+                    return false;
+                }
+                ((LegacyProtoLogImpl) mShellProtoLog).startProtoLog(pw);
                 return true;
             }
             case "stop": {
-                mShellProtoLog.stopProtoLog(pw, true /* writeToFile */);
+                if (android.tracing.Flags.perfettoProtolog()) {
+                    pw.println("(Deprecated) legacy command. Use Perfetto commands instead.");
+                    return false;
+                }
+                ((LegacyProtoLogImpl) mShellProtoLog).stopProtoLog(pw, true);
                 return true;
             }
             case "enable-text": {
                 String[] groups = Arrays.copyOfRange(args, 1, args.length);
-                int result = mShellProtoLog.startTextLogging(groups, pw);
+                int result = mShellProtoLog.startLoggingToLogcat(groups, logger);
                 if (result == 0) {
                     pw.println("Starting logging on groups: " + Arrays.toString(groups));
                     return true;
@@ -69,7 +85,7 @@ public class ProtoLogController implements ShellCommandHandler.ShellCommandActio
             }
             case "disable-text": {
                 String[] groups = Arrays.copyOfRange(args, 1, args.length);
-                int result = mShellProtoLog.stopTextLogging(groups, pw);
+                int result = mShellProtoLog.stopLoggingToLogcat(groups, logger);
                 if (result == 0) {
                     pw.println("Stopping logging on groups: " + Arrays.toString(groups));
                     return true;
@@ -78,19 +94,23 @@ public class ProtoLogController implements ShellCommandHandler.ShellCommandActio
             }
             case "enable": {
                 String[] groups = Arrays.copyOfRange(args, 1, args.length);
-                return mShellProtoLog.startTextLogging(groups, pw) == 0;
+                return mShellProtoLog.startLoggingToLogcat(groups, logger) == 0;
             }
             case "disable": {
                 String[] groups = Arrays.copyOfRange(args, 1, args.length);
-                return mShellProtoLog.stopTextLogging(groups, pw) == 0;
+                return mShellProtoLog.stopLoggingToLogcat(groups, logger) == 0;
             }
             case "save-for-bugreport": {
+                if (android.tracing.Flags.perfettoProtolog()) {
+                    pw.println("(Deprecated) legacy command");
+                    return false;
+                }
                 if (!mShellProtoLog.isProtoEnabled()) {
                     pw.println("Logging to proto is not enabled for WMShell.");
                     return false;
                 }
-                mShellProtoLog.stopProtoLog(pw, true /* writeToFile */);
-                mShellProtoLog.startProtoLog(pw);
+                ((LegacyProtoLogImpl) mShellProtoLog).stopProtoLog(pw, true /* writeToFile */);
+                ((LegacyProtoLogImpl) mShellProtoLog).startProtoLog(pw);
                 return true;
             }
             default: {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
deleted file mode 100644
index 93ffb3dc8115e2750203900cf49f70f2145ead80..0000000000000000000000000000000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogImpl.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2020 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.wm.shell.protolog;
-
-import android.annotation.Nullable;
-
-import com.android.internal.protolog.BaseProtoLogImpl;
-import com.android.internal.protolog.ProtoLogViewerConfigReader;
-import com.android.internal.protolog.common.IProtoLogGroup;
-
-import java.io.File;
-import java.io.PrintWriter;
-
-
-/**
- * A service for the ProtoLog logging system.
- */
-public class ShellProtoLogImpl extends BaseProtoLogImpl {
-    private static final String TAG = "ProtoLogImpl";
-    private static final int BUFFER_CAPACITY = 1024 * 1024;
-    // TODO: find a proper location to save the protolog message file
-    private static final String LOG_FILENAME = "/data/misc/wmtrace/shell_log.winscope";
-    private static final String VIEWER_CONFIG_FILENAME = "/system_ext/etc/wmshell.protolog.json.gz";
-
-    private static ShellProtoLogImpl sServiceInstance = null;
-
-    static {
-        addLogGroupEnum(ShellProtoLogGroup.values());
-    }
-
-    /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void d(IProtoLogGroup group, int messageHash, int paramsMask,
-            @Nullable String messageString,
-            Object... args) {
-        getSingleInstance()
-                .log(LogLevel.DEBUG, group, messageHash, paramsMask, messageString, args);
-    }
-
-    /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void v(IProtoLogGroup group, int messageHash, int paramsMask,
-            @Nullable String messageString,
-            Object... args) {
-        getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString,
-                args);
-    }
-
-    /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void i(IProtoLogGroup group, int messageHash, int paramsMask,
-            @Nullable String messageString,
-            Object... args) {
-        getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);
-    }
-
-    /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void w(IProtoLogGroup group, int messageHash, int paramsMask,
-            @Nullable String messageString,
-            Object... args) {
-        getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args);
-    }
-
-    /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void e(IProtoLogGroup group, int messageHash, int paramsMask,
-            @Nullable String messageString,
-            Object... args) {
-        getSingleInstance()
-                .log(LogLevel.ERROR, group, messageHash, paramsMask, messageString, args);
-    }
-
-    /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
-    public static void wtf(IProtoLogGroup group, int messageHash, int paramsMask,
-            @Nullable String messageString,
-            Object... args) {
-        getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args);
-    }
-
-    /** Returns true iff logging is enabled for the given {@code IProtoLogGroup}. */
-    public static boolean isEnabled(IProtoLogGroup group) {
-        return group.isLogToLogcat()
-                || (group.isLogToProto() && getSingleInstance().isProtoEnabled());
-    }
-
-    /**
-     * Returns the single instance of the ProtoLogImpl singleton class.
-     */
-    public static synchronized ShellProtoLogImpl getSingleInstance() {
-        if (sServiceInstance == null) {
-            sServiceInstance = new ShellProtoLogImpl();
-        }
-        return sServiceInstance;
-    }
-
-    public int startTextLogging(String[] groups, PrintWriter pw) {
-        mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME);
-        return setLogging(true /* setTextLogging */, true, pw, groups);
-    }
-
-    public int stopTextLogging(String[] groups, PrintWriter pw) {
-        return setLogging(true /* setTextLogging */, false, pw, groups);
-    }
-
-    private ShellProtoLogImpl() {
-        super(new File(LOG_FILENAME), VIEWER_CONFIG_FILENAME, BUFFER_CAPACITY,
-                new ProtoLogViewerConfigReader());
-    }
-}
-
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
index 9b48a542720c205bf27cc4339e82c3c062609b3d..7a50814f0275b83bcbea3c892db6eb804b1d6432 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
@@ -18,7 +18,7 @@ package com.android.wm.shell.util
 
 import android.util.Log
 import com.android.internal.protolog.common.IProtoLogGroup
-import com.android.wm.shell.protolog.ShellProtoLogImpl
+import com.android.internal.protolog.common.ProtoLog
 
 /**
  * Log messages using an API similar to [com.android.internal.protolog.common.ProtoLog]. Useful for
@@ -31,42 +31,42 @@ class KtProtoLog {
     companion object {
         /** @see [com.android.internal.protolog.common.ProtoLog.d] */
         fun d(group: IProtoLogGroup, messageString: String, vararg args: Any) {
-            if (ShellProtoLogImpl.isEnabled(group)) {
+            if (ProtoLog.isEnabled(group)) {
                 Log.d(group.tag, String.format(messageString, *args))
             }
         }
 
         /** @see [com.android.internal.protolog.common.ProtoLog.v] */
         fun v(group: IProtoLogGroup, messageString: String, vararg args: Any) {
-            if (ShellProtoLogImpl.isEnabled(group)) {
+            if (ProtoLog.isEnabled(group)) {
                 Log.v(group.tag, String.format(messageString, *args))
             }
         }
 
         /** @see [com.android.internal.protolog.common.ProtoLog.i] */
         fun i(group: IProtoLogGroup, messageString: String, vararg args: Any) {
-            if (ShellProtoLogImpl.isEnabled(group)) {
+            if (ProtoLog.isEnabled(group)) {
                 Log.i(group.tag, String.format(messageString, *args))
             }
         }
 
         /** @see [com.android.internal.protolog.common.ProtoLog.w] */
         fun w(group: IProtoLogGroup, messageString: String, vararg args: Any) {
-            if (ShellProtoLogImpl.isEnabled(group)) {
+            if (ProtoLog.isEnabled(group)) {
                 Log.w(group.tag, String.format(messageString, *args))
             }
         }
 
         /** @see [com.android.internal.protolog.common.ProtoLog.e] */
         fun e(group: IProtoLogGroup, messageString: String, vararg args: Any) {
-            if (ShellProtoLogImpl.isEnabled(group)) {
+            if (ProtoLog.isEnabled(group)) {
                 Log.e(group.tag, String.format(messageString, *args))
             }
         }
 
         /** @see [com.android.internal.protolog.common.ProtoLog.wtf] */
         fun wtf(group: IProtoLogGroup, messageString: String, vararg args: Any) {
-            if (ShellProtoLogImpl.isEnabled(group)) {
+            if (ProtoLog.isEnabled(group)) {
                 Log.wtf(group.tag, String.format(messageString, *args))
             }
         }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d1a3571a87c1bb7b254b5e77af2f62061c7a451e..bed95a584c27cf51340ef76359543a7f2923eab7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -550,5 +550,6 @@ android_app {
     required: [
         "privapp_whitelist_com.android.systemui",
         "wmshell.protolog.json.gz",
+        "wmshell.protolog.pb",
     ],
 }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 7940ca64b330bcf44f171d2e28aa43827f4a77f9..94aab7599bb0d35941782fc2de0dafac768d1956 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -41,16 +41,18 @@ java_library_static {
 genrule {
     name: "services.core.protologsrc",
     srcs: [
+        ":protolog-impl",
         ":protolog-groups",
         ":services.core-sources-am-wm",
     ],
     tools: ["protologtool"],
     cmd: "$(location protologtool) transform-protolog-calls " +
         "--protolog-class com.android.internal.protolog.common.ProtoLog " +
-        "--protolog-impl-class com.android.internal.protolog.ProtoLogImpl " +
-        "--protolog-cache-class 'com.android.server.wm.ProtoLogCache' " +
         "--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
         "--loggroups-jar $(location :protolog-groups) " +
+        "--viewer-config-file-path /etc/core.protolog.pb " +
+        "--legacy-viewer-config-file-path /system/etc/protolog.conf.json.gz " +
+        "--legacy-output-file-path /data/misc/wmtrace/wm_log.winscope " +
         "--output-srcjar $(out) " +
         "$(locations :services.core-sources-am-wm)",
     out: ["services.core.protolog.srcjar"],
@@ -67,25 +69,43 @@ genrule {
         "--protolog-class com.android.internal.protolog.common.ProtoLog " +
         "--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
         "--loggroups-jar $(location :protolog-groups) " +
-        "--viewer-conf $(out) " +
+        "--viewer-config-type json " +
+        "--viewer-config $(out) " +
         "$(locations :services.core-sources-am-wm)",
     out: ["services.core.protolog.json"],
 }
 
 genrule {
-    name: "checked-protolog.json",
+    name: "gen-core.protolog.pb",
     srcs: [
-        ":generate-protolog.json",
-        ":services.core.protolog.json",
+        ":protolog-groups",
+        ":services.core-sources-am-wm",
     ],
-    cmd: "cp $(location :generate-protolog.json) $(out) && " +
-        "{ ! (diff $(out) $(location :services.core.protolog.json) | grep -q '^<') || " +
+    tools: ["protologtool"],
+    cmd: "$(location protologtool) generate-viewer-config " +
+        "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+        "--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
+        "--loggroups-jar $(location :protolog-groups) " +
+        "--viewer-config-type proto " +
+        "--viewer-config $(out) " +
+        "$(locations :services.core-sources-am-wm)",
+    out: ["core.protolog.pb"],
+}
+
+genrule {
+    name: "checked-core.protolog.pb",
+    srcs: [
+        ":gen-core.protolog.pb",
+        ":file-core.protolog.pb",
+    ],
+    cmd: "cp $(location :gen-core.protolog.pb) $(out) && " +
+        "{ ! (diff $(out) $(location :file-core.protolog.pb) | grep -q '^<') || " +
         "{ echo -e '\\n\\n################################################################\\n#\\n" +
         "#  ERROR: ProtoLog viewer config is stale.  To update it, run:\\n#\\n" +
-        "#  cp $${ANDROID_BUILD_TOP}/$(location :generate-protolog.json) " +
-        "$${ANDROID_BUILD_TOP}/$(location :services.core.protolog.json)\\n#\\n" +
+        "#  cp $${ANDROID_BUILD_TOP}/$(location :gen-core.protolog.pb) " +
+        "$${ANDROID_BUILD_TOP}/$(location :file-core.protolog.pb)\\n#\\n" +
         "################################################################\\n\\n' >&2 && false; } }",
-    out: ["services.core.protolog.json"],
+    out: ["core.protolog.pb"],
 }
 
 genrule {
@@ -157,7 +177,7 @@ java_library_static {
     required: [
         "default_television.xml",
         "gps_debug.conf",
-        "protolog.conf.json.gz",
+        "core.protolog.pb",
     ],
 
     static_libs: [
@@ -258,14 +278,7 @@ prebuilt_etc {
     src: "java/com/android/server/location/gnss/gps_debug.conf",
 }
 
-genrule {
-    name: "services.core.json.gz",
-    srcs: [":checked-protolog.json"],
-    out: ["services.core.protolog.json.gz"],
-    cmd: "gzip -c < $(in) > $(out)",
-}
-
 prebuilt_etc {
-    name: "protolog.conf.json.gz",
-    src: ":services.core.json.gz",
+    name: "core.protolog.pb",
+    src: ":checked-core.protolog.pb",
 }
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 939cf1ae471be10ed51d2e4eb435d6cbd6025948..1a63f14e1b8c827f98d61c8416abd462b9c82568 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -137,7 +137,6 @@ import android.view.animation.TranslateAnimation;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.TransitionAnimation;
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.DumpUtils.Dump;
 import com.android.internal.util.function.pooled.PooledLambda;
@@ -248,7 +247,7 @@ public class AppTransition implements Dump {
         mHandler = new Handler(service.mH.getLooper());
         mDisplayContent = displayContent;
         mTransitionAnimation = new TransitionAnimation(
-                context, ProtoLogImpl.isEnabled(WM_DEBUG_ANIM), TAG);
+                context, ProtoLog.isEnabled(WM_DEBUG_ANIM), TAG);
 
         mGridLayoutRecentsEnabled = SystemProperties.getBoolean("ro.recents.grid", false);
 
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index a98b9f7cd1d1b6babc5a00e99fc2b4a7ee13b19c..3ef6eeb2ecf36da8e73823e952f6da524cee9f24 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -44,7 +44,6 @@ import android.view.SurfaceControl.Transaction;
 import android.view.WindowManager;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.FastPrintWriter;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
@@ -210,7 +209,7 @@ class RemoteAnimationController implements DeathRecipient {
                 Slog.e(TAG, "Failed to start remote animation", e);
                 onAnimationFinished();
             }
-            if (ProtoLogImpl.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS)) {
+            if (ProtoLog.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS)) {
                 ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation(): Notify animation start:");
                 writeStartDebugStatement();
             }
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index c3de4d5acc21905b134077c3075a917e6452e29e..d67684c7038e6233fd4a097998d6fef5456681fa 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -32,7 +32,6 @@ import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 
 import java.io.PrintWriter;
@@ -193,7 +192,7 @@ public class SurfaceAnimator {
             return;
         }
         mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
-        if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+        if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
             StringWriter sw = new StringWriter();
             PrintWriter pw = new PrintWriter(sw);
             mAnimation.dump(pw, "");
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 001f46d4c36cc2a8420eb9b020b47c1ced922290..594043d380c92091c265dfb3cb7a43b07eb082e5 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -58,7 +58,6 @@ import android.window.ScreenCapture;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ToBooleanFunction;
 import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
@@ -336,7 +335,7 @@ class WallpaperController {
         for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) {
             final WallpaperWindowToken token = mWallpaperTokens.get(i);
             token.setVisibility(false);
-            if (ProtoLogImpl.isEnabled(WM_DEBUG_WALLPAPER) && token.isVisible()) {
+            if (ProtoLog.isEnabled(WM_DEBUG_WALLPAPER) && token.isVisible()) {
                 ProtoLog.d(WM_DEBUG_WALLPAPER,
                         "Hiding wallpaper %s from %s target=%s prev=%s callers=%s",
                         token, winGoingAway, mWallpaperTarget, mPrevWallpaperTarget,
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 61fde5e31e8fba8e2759da31fe481c9ea49f1907..fd0289edc84b45154e2ea5c4d7ff8525ab557656 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -113,7 +113,6 @@ import android.window.WindowContainerToken;
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ToBooleanFunction;
 import com.android.server.wm.SurfaceAnimator.Animatable;
@@ -3410,7 +3409,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
                 // ActivityOption#makeCustomAnimation or WindowManager#overridePendingTransition.
                 a.restrictDuration(MAX_APP_TRANSITION_DURATION);
             }
-            if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+            if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
                 ProtoLog.i(WM_DEBUG_ANIM, "Loaded animation %s for %s, duration: %d, stack=%s",
                         a, this, ((a != null) ? a.getDuration() : 0), Debug.getCallers(20));
             }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9b7bc4383e0c3f9aed3ce71fcca377d1645a607c..08d43ae6f4c9cf18016c6540302b45745ca28b50 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -327,8 +327,8 @@ import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.policy.IKeyguardLockedStateListener;
 import com.android.internal.policy.IShortcutService;
 import com.android.internal.policy.KeyInterceptionInfo;
+import com.android.internal.protolog.LegacyProtoLogImpl;
 import com.android.internal.protolog.ProtoLogGroup;
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastPrintWriter;
@@ -6701,7 +6701,11 @@ public class WindowManagerService extends IWindowManager.Stub
 
     private void dumpLogStatus(PrintWriter pw) {
         pw.println("WINDOW MANAGER LOGGING (dumpsys window logging)");
-        pw.println(ProtoLogImpl.getSingleInstance().getStatus());
+        if (android.tracing.Flags.perfettoProtolog()) {
+            pw.println("Deprecated legacy command. Use Perfetto commands instead.");
+            return;
+        }
+        ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).getStatus();
     }
 
     private void dumpSessionsLocked(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 8fad9509af44520b76272f13e77a217ce801a3f3..0b29f9688acd614f7bceadffe66cf42642a1df1b 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -48,7 +48,9 @@ import android.view.IWindowManager;
 import android.view.ViewDebug;
 
 import com.android.internal.os.ByteTransferPipe;
-import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.LegacyProtoLogImpl;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.IoThread;
 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
 import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition;
@@ -107,11 +109,19 @@ public class WindowManagerShellCommand extends ShellCommand {
                     // trace files can be written.
                     return mInternal.mWindowTracing.onShellCommand(this);
                 case "logging":
-                    int result = ProtoLogImpl.getSingleInstance().onShellCommand(this);
-                    if (result != 0) {
-                        pw.println("Not handled, please use "
-                                + "`adb shell dumpsys activity service SystemUIService WMShell` "
-                                + "if you are looking for ProtoLog in WMShell");
+                    IProtoLog instance = ProtoLog.getSingleInstance();
+                    int result = 0;
+                    if (instance instanceof LegacyProtoLogImpl) {
+                        result = ((LegacyProtoLogImpl) instance).onShellCommand(this);
+                        if (result != 0) {
+                            pw.println("Not handled, please use "
+                                    + "`adb shell dumpsys activity service SystemUIService "
+                                    + "WMShell` if you are looking for ProtoLog in WMShell");
+                        }
+                    } else {
+                        result = -1;
+                        pw.println("Command not supported. "
+                                + "Only supported when using legacy ProtoLog.");
                     }
                     return result;
                 case "user-rotation":
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 90f5b62b4a0810f4d2988f671871191c5643262d..a7a28c282ff90c94c0ff0e117936c3bddb5d802b 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -245,7 +245,6 @@ import android.window.OnBackInvokedCallbackInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.KeyInterceptionInfo;
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.ToBooleanFunction;
@@ -4681,7 +4680,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
     }
 
     void onExitAnimationDone() {
-        if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+        if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
             final AnimationAdapter animationAdapter = mSurfaceAnimator.getAnimation();
             StringWriter sw = new StringWriter();
             if (animationAdapter != null) {
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 6428591d8b8d0852a6e85d0085d6c2220d4f9c98..7f7c2493cd68c4dcbbfcaf76bc918586d6888a08 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -60,7 +60,6 @@ import android.view.WindowManager.LayoutParams;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 
-import com.android.internal.protolog.ProtoLogImpl;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.policy.WindowManagerPolicy;
 
@@ -584,7 +583,7 @@ class WindowStateAnimator {
                             mWin.mAttrs, attr, TRANSIT_OLD_NONE);
                 }
             }
-            if (ProtoLogImpl.isEnabled(WM_DEBUG_ANIM)) {
+            if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) {
                 ProtoLog.v(WM_DEBUG_ANIM, "applyAnimation: win=%s"
                         + " anim=%d attr=0x%x a=%s transit=%d type=%d isEntrance=%b Callers %s",
                         this, anim, attr, a, transit, mAttrType, isEntrance, Debug.getCallers(20));
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index 416d0427a0d6fdd18901db9c1075c917f94022ac..424d504340109ba05e5e5a6269bda4c669d9441a 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -35,7 +35,9 @@ import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 import android.view.Choreographer;
 
-import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.LegacyProtoLogImpl;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.TraceBuffer;
 
 import java.io.File;
@@ -77,6 +79,8 @@ class WindowTracing {
     private volatile boolean mEnabledLockFree;
     private boolean mScheduled;
 
+    private final IProtoLog mProtoLog;
+
     static WindowTracing createDefaultAndStartLooper(WindowManagerService service,
             Choreographer choreographer) {
         File file = new File(TRACE_FILENAME);
@@ -96,6 +100,7 @@ class WindowTracing {
         mTraceFile = file;
         mBuffer = new TraceBuffer(bufferCapacity);
         setLogLevel(WindowTraceLogLevel.TRIM, null /* pw */);
+        mProtoLog = ProtoLog.getSingleInstance();
     }
 
     void startTrace(@Nullable PrintWriter pw) {
@@ -104,7 +109,6 @@ class WindowTracing {
             return;
         }
         synchronized (mEnabledLock) {
-            ProtoLogImpl.getSingleInstance().startProtoLog(pw);
             logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
             mBuffer.resetBuffer();
             mEnabled = mEnabledLockFree = true;
@@ -132,7 +136,6 @@ class WindowTracing {
             writeTraceToFileLocked();
             logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
         }
-        ProtoLogImpl.getSingleInstance().stopProtoLog(pw, true);
     }
 
     /**
@@ -152,11 +155,15 @@ class WindowTracing {
             logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
             writeTraceToFileLocked();
             logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
-            ProtoLogImpl.getSingleInstance().stopProtoLog(pw, true);
+            if (!android.tracing.Flags.perfettoProtolog()) {
+                ((LegacyProtoLogImpl) mProtoLog).stopProtoLog(pw, true);
+            }
             logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
             mBuffer.resetBuffer();
             mEnabled = mEnabledLockFree = true;
-            ProtoLogImpl.getSingleInstance().startProtoLog(pw);
+            if (!android.tracing.Flags.perfettoProtolog()) {
+                ((LegacyProtoLogImpl) mProtoLog).startProtoLog(pw);
+            }
         }
     }
 
diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp
index e83f03d155aa2d8a41348374b2e79b47050c3a50..b2922945aff9d4015c8810596919f55b13e8f661 100644
--- a/services/tests/wmtests/Android.bp
+++ b/services/tests/wmtests/Android.bp
@@ -22,16 +22,21 @@ filegroup {
 genrule {
     name: "wmtests.protologsrc",
     srcs: [
+        ":protolog-impl",
         ":protolog-groups",
         ":wmtests-sources",
     ],
     tools: ["protologtool"],
     cmd: "$(location protologtool) transform-protolog-calls " +
         "--protolog-class com.android.internal.protolog.common.ProtoLog " +
-        "--protolog-impl-class com.android.internal.protolog.ProtoLogImpl " +
-        "--protolog-cache-class 'com.android.server.wm.ProtoLogCache' " +
         "--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
         "--loggroups-jar $(location :protolog-groups) " +
+        // Used for the ProtoLogIntegrationTest, where don't test decoding or writing to file
+        // so the parameters below are irrelevant.
+        "--viewer-config-file-path /some/unused/file/path.pb " +
+        "--legacy-viewer-config-file-path /some/unused/file/path.json.gz " +
+        "--legacy-output-file-path /some/unused/file/path.winscope " +
+        // END of irrelevant params.
         "--output-srcjar $(out) " +
         "$(locations :wmtests-sources)",
     out: ["wmtests.protolog.srcjar"],
@@ -42,7 +47,7 @@ android_test {
 
     // We only want this apk build for tests.
     srcs: [
-        ":wmtests.protologsrc",
+        ":wmtests-sources",
         "src/**/*.aidl",
     ],
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
index af0d32c764a4c7782151a779c8caa885673f0530..c5bf78bb60b5d749aec24532ca24c7710aabdc46 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java
@@ -27,6 +27,8 @@ import androidx.test.filters.SmallTest;
 
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.common.ProtoLog;
 
 import org.junit.After;
@@ -47,9 +49,9 @@ public class ProtoLogIntegrationTest {
     @Ignore("b/163095037")
     @Test
     public void testProtoLogToolIntegration() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         runWith(mockedProtoLog, this::testProtoLog);
-        verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.ERROR), eq(ProtoLogGroup.TEST_GROUP),
+        verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq(ProtoLogGroup.TEST_GROUP),
                 anyInt(), eq(0b0010010111),
                 eq(com.android.internal.protolog.ProtoLogGroup.TEST_GROUP.isLogToLogcat()
                         ? "Test completed successfully: %b %d %x %f %% %s"
@@ -66,18 +68,13 @@ public class ProtoLogIntegrationTest {
     /**
      * Starts protolog for the duration of {@code runnable}, with a ProtoLogImpl instance installed.
      */
-    private void runWith(ProtoLogImpl mockInstance, Runnable runnable) {
-        ProtoLogImpl original = ProtoLogImpl.getSingleInstance();
-        original.startProtoLog(null);
+    private void runWith(IProtoLog mockInstance, Runnable runnable) {
+        IProtoLog original = ProtoLog.getSingleInstance();
+        ProtoLogImpl.setSingleInstance(mockInstance);
         try {
-            ProtoLogImpl.setSingleInstance(mockInstance);
-            try {
-                runnable.run();
-            } finally {
-                ProtoLogImpl.setSingleInstance(original);
-            }
+            runnable.run();
         } finally {
-            original.stopProtoLog(null, false);
+            ProtoLogImpl.setSingleInstance(original);
         }
     }
 }
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index a4877999ff6f342d34597dc1cb7ef93edb4b2b34..827ff4fbd98993e33b41002992ba892d3ca23d81 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -21,6 +21,9 @@ android_test {
         "mockito-target-minus-junit4",
         "truth",
         "platform-test-annotations",
+        "flickerlib-parsers",
+        "perfetto_trace_java_protos",
+        "flickerlib-trace_processor_shell",
     ],
     java_resource_dirs: ["res"],
     certificate: "platform",
diff --git a/tests/Internal/AndroidManifest.xml b/tests/Internal/AndroidManifest.xml
index dbba24531769ed6957fce89e1d75a563e015b2ac..9a3fe617e70ab5b3df8fd4c6e40261719339b08f 100644
--- a/tests/Internal/AndroidManifest.xml
+++ b/tests/Internal/AndroidManifest.xml
@@ -19,7 +19,11 @@
      package="com.android.internal.tests">
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.BIND_WALLPAPER"/>
-    <application>
+    <!-- Allow the test to connect to perfetto trace processor -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <application
+        android:requestLegacyExternalStorage="true"
+        android:networkSecurityConfig="@xml/network_security_config">
         <uses-library android:name="android.test.runner"/>
 
         <service android:name="stub.DummyWallpaperService"
diff --git a/tests/Internal/res/xml/network_security_config.xml b/tests/Internal/res/xml/network_security_config.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fdf1dbbe7672824419c9dfc64da2b0cb64eb2d36
--- /dev/null
+++ b/tests/Internal/res/xml/network_security_config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">localhost</domain>
+    </domain-config>
+</network-security-config>
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a64996c6838e835fcc55117160236371f4433a8c
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.internal.protolog;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.internal.protolog.LegacyProtoLogImpl.PROTOLOG_VERSION;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.util.proto.ProtoInputStream;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogLevel;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.LinkedList;
+
+/**
+ * Test class for {@link ProtoLogImpl}.
+ */
+@SuppressWarnings("ConstantConditions")
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class LegacyProtoLogImplTest {
+
+    private static final byte[] MAGIC_HEADER = new byte[]{
+            0x9, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x4c, 0x4f, 0x47
+    };
+
+    private LegacyProtoLogImpl mProtoLog;
+    private File mFile;
+
+    @Mock
+    private LegacyProtoLogViewerConfigReader mReader;
+
+    private final String mViewerConfigFilename = "unused/file/path";
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        final Context testContext = getInstrumentation().getContext();
+        mFile = testContext.getFileStreamPath("tracing_test.dat");
+        //noinspection ResultOfMethodCallIgnored
+        mFile.delete();
+        mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename,
+                1024 * 1024, mReader, 1024);
+    }
+
+    @After
+    public void tearDown() {
+        if (mFile != null) {
+            //noinspection ResultOfMethodCallIgnored
+            mFile.delete();
+        }
+        ProtoLogImpl.setSingleInstance(null);
+    }
+
+    @Test
+    public void isEnabled_returnsFalseByDefault() {
+        assertFalse(mProtoLog.isProtoEnabled());
+    }
+
+    @Test
+    public void isEnabled_returnsTrueAfterStart() {
+        mProtoLog.startProtoLog(mock(PrintWriter.class));
+        assertTrue(mProtoLog.isProtoEnabled());
+    }
+
+    @Test
+    public void isEnabled_returnsFalseAfterStop() {
+        mProtoLog.startProtoLog(mock(PrintWriter.class));
+        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+        assertFalse(mProtoLog.isProtoEnabled());
+    }
+
+    @Test
+    public void logFile_startsWithMagicHeader() throws Exception {
+        mProtoLog.startProtoLog(mock(PrintWriter.class));
+        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+
+        assertTrue("Log file should exist", mFile.exists());
+
+        byte[] header = new byte[MAGIC_HEADER.length];
+        try (InputStream is = new FileInputStream(mFile)) {
+            assertEquals(MAGIC_HEADER.length, is.read(header));
+            assertArrayEquals(MAGIC_HEADER, header);
+        }
+    }
+
+    @Test
+    public void log_logcatEnabledExternalMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f");
+        LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+                new Object[]{true, 10000, 30000, "test", 0.000003});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO),
+                eq("test true 10000 % 0x7530 test 3.0E-6"));
+        verify(mReader).getViewerString(eq(1234L));
+    }
+
+    @Test
+    public void log_logcatEnabledInvalidMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f");
+        LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+                new Object[]{true, 10000, 0.0001, 0.00002, "test"});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO),
+                eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test"));
+        verify(mReader).getViewerString(eq(1234L));
+    }
+
+    @Test
+    public void log_logcatEnabledInlineMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+        LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+                new Object[]{5});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO), eq("test 5"));
+        verify(mReader, never()).getViewerString(anyLong());
+    }
+
+    @Test
+    public void log_logcatEnabledNoMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn(null);
+        LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+                new Object[]{5});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5"));
+        verify(mReader).getViewerString(eq(1234L));
+    }
+
+    @Test
+    public void log_logcatDisabled() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+        LegacyProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+                new Object[]{5});
+
+        verify(implSpy, never()).passToLogcat(any(), any(), any());
+        verify(mReader, never()).getViewerString(anyLong());
+    }
+
+    private static class ProtoLogData {
+        Long mMessageHash = null;
+        Long mElapsedTime = null;
+        LinkedList<String> mStrParams = new LinkedList<>();
+        LinkedList<Long> mSint64Params = new LinkedList<>();
+        LinkedList<Double> mDoubleParams = new LinkedList<>();
+        LinkedList<Boolean> mBooleanParams = new LinkedList<>();
+    }
+
+    private ProtoLogData readProtoLogSingle(ProtoInputStream ip) throws IOException {
+        while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            if (ip.getFieldNumber() == (int) ProtoLogFileProto.VERSION) {
+                assertEquals(PROTOLOG_VERSION, ip.readString(ProtoLogFileProto.VERSION));
+                continue;
+            }
+            if (ip.getFieldNumber() != (int) ProtoLogFileProto.LOG) {
+                continue;
+            }
+            long token = ip.start(ProtoLogFileProto.LOG);
+            ProtoLogData data = new ProtoLogData();
+            while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                switch (ip.getFieldNumber()) {
+                    case (int) ProtoLogMessage.MESSAGE_HASH: {
+                        data.mMessageHash = ip.readLong(ProtoLogMessage.MESSAGE_HASH);
+                        break;
+                    }
+                    case (int) ProtoLogMessage.ELAPSED_REALTIME_NANOS: {
+                        data.mElapsedTime = ip.readLong(ProtoLogMessage.ELAPSED_REALTIME_NANOS);
+                        break;
+                    }
+                    case (int) ProtoLogMessage.STR_PARAMS: {
+                        data.mStrParams.add(ip.readString(ProtoLogMessage.STR_PARAMS));
+                        break;
+                    }
+                    case (int) ProtoLogMessage.SINT64_PARAMS: {
+                        data.mSint64Params.add(ip.readLong(ProtoLogMessage.SINT64_PARAMS));
+                        break;
+                    }
+                    case (int) ProtoLogMessage.DOUBLE_PARAMS: {
+                        data.mDoubleParams.add(ip.readDouble(ProtoLogMessage.DOUBLE_PARAMS));
+                        break;
+                    }
+                    case (int) ProtoLogMessage.BOOLEAN_PARAMS: {
+                        data.mBooleanParams.add(ip.readBoolean(ProtoLogMessage.BOOLEAN_PARAMS));
+                        break;
+                    }
+                }
+            }
+            ip.end(token);
+            return data;
+        }
+        return null;
+    }
+
+    @Test
+    public void log_protoEnabled() throws Exception {
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
+        mProtoLog.startProtoLog(mock(PrintWriter.class));
+        long before = SystemClock.elapsedRealtimeNanos();
+        mProtoLog.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
+                0b1110101001010100, null,
+                new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
+        long after = SystemClock.elapsedRealtimeNanos();
+        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+        try (InputStream is = new FileInputStream(mFile)) {
+            ProtoInputStream ip = new ProtoInputStream(is);
+            ProtoLogData data = readProtoLogSingle(ip);
+            assertNotNull(data);
+            assertEquals(1234, data.mMessageHash.longValue());
+            assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
+            assertArrayEquals(new String[]{"test"}, data.mStrParams.toArray());
+            assertArrayEquals(new Long[]{1L, 2L, 3L}, data.mSint64Params.toArray());
+            assertArrayEquals(new Double[]{0.4, 0.5, 0.6}, data.mDoubleParams.toArray());
+            assertArrayEquals(new Boolean[]{true}, data.mBooleanParams.toArray());
+        }
+    }
+
+    @Test
+    public void log_invalidParamsMask() throws Exception {
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
+        mProtoLog.startProtoLog(mock(PrintWriter.class));
+        long before = SystemClock.elapsedRealtimeNanos();
+        mProtoLog.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
+                0b01100100, null,
+                new Object[]{"test", 1, 0.1, true});
+        long after = SystemClock.elapsedRealtimeNanos();
+        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+        try (InputStream is = new FileInputStream(mFile)) {
+            ProtoInputStream ip = new ProtoInputStream(is);
+            ProtoLogData data = readProtoLogSingle(ip);
+            assertNotNull(data);
+            assertEquals(1234, data.mMessageHash.longValue());
+            assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
+            assertArrayEquals(new String[]{"test", "(INVALID PARAMS_MASK) true"},
+                    data.mStrParams.toArray());
+            assertArrayEquals(new Long[]{1L}, data.mSint64Params.toArray());
+            assertArrayEquals(new Double[]{0.1}, data.mDoubleParams.toArray());
+            assertArrayEquals(new Boolean[]{}, data.mBooleanParams.toArray());
+        }
+    }
+
+    @Test
+    public void log_protoDisabled() throws Exception {
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+        mProtoLog.startProtoLog(mock(PrintWriter.class));
+        mProtoLog.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
+                0b11, null, new Object[]{true});
+        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
+        try (InputStream is = new FileInputStream(mFile)) {
+            ProtoInputStream ip = new ProtoInputStream(is);
+            ProtoLogData data = readProtoLogSingle(ip);
+            assertNull(data);
+        }
+    }
+
+    private enum TestProtoLogGroup implements IProtoLogGroup {
+        TEST_GROUP(true, true, false, "WindowManagetProtoLogTest");
+
+        private final boolean mEnabled;
+        private volatile boolean mLogToProto;
+        private volatile boolean mLogToLogcat;
+        private final String mTag;
+
+        /**
+         * @param enabled     set to false to exclude all log statements for this group from
+         *                    compilation,
+         *                    they will not be available in runtime.
+         * @param logToProto  enable binary logging for the group
+         * @param logToLogcat enable text logging for the group
+         * @param tag         name of the source of the logged message
+         */
+        TestProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) {
+            this.mEnabled = enabled;
+            this.mLogToProto = logToProto;
+            this.mLogToLogcat = logToLogcat;
+            this.mTag = tag;
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return mEnabled;
+        }
+
+        @Override
+        public boolean isLogToProto() {
+            return mLogToProto;
+        }
+
+        @Override
+        public boolean isLogToLogcat() {
+            return mLogToLogcat;
+        }
+
+        @Override
+        public boolean isLogToAny() {
+            return mLogToLogcat || mLogToProto;
+        }
+
+        @Override
+        public String getTag() {
+            return mTag;
+        }
+
+        @Override
+        public void setLogToProto(boolean logToProto) {
+            this.mLogToProto = logToProto;
+        }
+
+        @Override
+        public void setLogToLogcat(boolean logToLogcat) {
+            this.mLogToLogcat = logToLogcat;
+        }
+
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b9f1738f9bb71853243a8a746198679bd3033f90
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import android.tracing.perfetto.CreateTlsStateArgs;
+import android.util.proto.ProtoInputStream;
+
+import com.android.internal.protolog.common.LogLevel;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import perfetto.protos.DataSourceConfigOuterClass;
+import perfetto.protos.ProtologCommon;
+import perfetto.protos.ProtologConfig;
+
+public class PerfettoDataSourceTest {
+    @Before
+    public void before() {
+        assumeTrue(android.tracing.Flags.perfettoProtolog());
+    }
+
+    @Test
+    public void noConfig() {
+        final ProtoLogDataSource.TlsState tlsState = createTlsState(
+                DataSourceConfigOuterClass.DataSourceConfig.newBuilder().build());
+
+        Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.WTF);
+        Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isFalse();
+    }
+
+    @Test
+    public void defaultTraceMode() {
+        final ProtoLogDataSource.TlsState tlsState = createTlsState(
+                DataSourceConfigOuterClass.DataSourceConfig.newBuilder()
+                        .setProtologConfig(
+                                ProtologConfig.ProtoLogConfig.newBuilder()
+                                        .setTracingMode(
+                                                ProtologConfig.ProtoLogConfig.TracingMode
+                                                        .ENABLE_ALL)
+                                        .build()
+                        ).build());
+
+        Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.DEBUG);
+        Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isFalse();
+    }
+
+    @Test
+    public void allEnabledTraceMode() {
+        final ProtoLogDataSource ds = new ProtoLogDataSource(() -> {}, () -> {}, () -> {});
+
+        final ProtoLogDataSource.TlsState tlsState = createTlsState(
+                DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig(
+                        ProtologConfig.ProtoLogConfig.newBuilder()
+                                .setTracingMode(
+                                        ProtologConfig.ProtoLogConfig.TracingMode.ENABLE_ALL)
+                                .build()
+                ).build()
+        );
+
+        Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.DEBUG);
+        Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isFalse();
+    }
+
+    @Test
+    public void requireGroupTagInOverrides() {
+        Exception exception = assertThrows(RuntimeException.class, () -> {
+            createTlsState(DataSourceConfigOuterClass.DataSourceConfig.newBuilder()
+                    .setProtologConfig(
+                            ProtologConfig.ProtoLogConfig.newBuilder()
+                                    .addGroupOverrides(
+                                            ProtologConfig.ProtoLogGroup.newBuilder()
+                                                    .setLogFrom(
+                                                            ProtologCommon.ProtoLogLevel
+                                                                    .PROTOLOG_LEVEL_WARN)
+                                                    .setCollectStacktrace(true)
+                                    )
+                                    .build()
+                    ).build());
+        });
+
+        Truth.assertThat(exception).hasMessageThat().contains("group override without a group tag");
+    }
+
+    @Test
+    public void stackTraceCollection() {
+        final ProtoLogDataSource.TlsState tlsState = createTlsState(
+                DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig(
+                        ProtologConfig.ProtoLogConfig.newBuilder()
+                                .addGroupOverrides(
+                                        ProtologConfig.ProtoLogGroup.newBuilder()
+                                                .setGroupName("SOME_TAG")
+                                                .setCollectStacktrace(true)
+                                )
+                                .build()
+                ).build());
+
+        Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isTrue();
+    }
+
+    @Test
+    public void groupLogFromOverrides() {
+        final ProtoLogDataSource.TlsState tlsState = createTlsState(
+                DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig(
+                        ProtologConfig.ProtoLogConfig.newBuilder()
+                                .addGroupOverrides(
+                                        ProtologConfig.ProtoLogGroup.newBuilder()
+                                                .setGroupName("SOME_TAG")
+                                                .setLogFrom(
+                                                        ProtologCommon.ProtoLogLevel
+                                                                .PROTOLOG_LEVEL_DEBUG)
+                                                .setCollectStacktrace(true)
+                                )
+                                .addGroupOverrides(
+                                        ProtologConfig.ProtoLogGroup.newBuilder()
+                                                .setGroupName("SOME_OTHER_TAG")
+                                                .setLogFrom(
+                                                        ProtologCommon.ProtoLogLevel
+                                                                .PROTOLOG_LEVEL_WARN)
+                                )
+                                .build()
+                ).build());
+
+        Truth.assertThat(tlsState.getLogFromLevel("SOME_TAG")).isEqualTo(LogLevel.DEBUG);
+        Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_TAG")).isTrue();
+
+        Truth.assertThat(tlsState.getLogFromLevel("SOME_OTHER_TAG")).isEqualTo(LogLevel.WARN);
+        Truth.assertThat(tlsState.getShouldCollectStacktrace("SOME_OTHER_TAG")).isFalse();
+
+        Truth.assertThat(tlsState.getLogFromLevel("UNKNOWN_TAG")).isEqualTo(LogLevel.WTF);
+        Truth.assertThat(tlsState.getShouldCollectStacktrace("UNKNOWN_TAG")).isFalse();
+    }
+
+    private ProtoLogDataSource.TlsState createTlsState(
+            DataSourceConfigOuterClass.DataSourceConfig config) {
+        final ProtoLogDataSource ds =
+                Mockito.spy(new ProtoLogDataSource(() -> {}, () -> {}, () -> {}));
+
+        ProtoInputStream configStream = new ProtoInputStream(config.toByteArray());
+        final ProtoLogDataSource.Instance dsInstance = Mockito.spy(
+                ds.createInstance(configStream, 8));
+        Mockito.doNothing().when(dsInstance).release();
+        final CreateTlsStateArgs mockCreateTlsStateArgs = Mockito.mock(CreateTlsStateArgs.class);
+        Mockito.when(mockCreateTlsStateArgs.getDataSourceInstanceLocked()).thenReturn(dsInstance);
+        return ds.createTlsState(mockCreateTlsStateArgs);
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c311050568bfc2d22593b36bdae2760c1ab5ccf
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -0,0 +1,586 @@
+/*
+ * 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;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.io.File.createTempFile;
+import static java.nio.file.Files.createTempDirectory;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.tools.common.ScenarioBuilder;
+import android.tools.common.traces.protolog.ProtoLogTrace;
+import android.tools.device.traces.TraceConfig;
+import android.tools.device.traces.TraceConfigs;
+import android.tools.device.traces.io.ResultReader;
+import android.tools.device.traces.io.ResultWriter;
+import android.tools.device.traces.monitors.PerfettoTraceMonitor;
+import android.tracing.perfetto.DataSource;
+import android.util.proto.ProtoInputStream;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogDataType;
+import com.android.internal.protolog.common.LogLevel;
+
+import com.google.common.truth.Truth;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Random;
+
+import perfetto.protos.Protolog;
+import perfetto.protos.ProtologCommon;
+
+/**
+ * Test class for {@link ProtoLogImpl}.
+ */
+@SuppressWarnings("ConstantConditions")
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class PerfettoProtoLogImplTest {
+    private final File mTracingDirectory = createTempDirectory("temp").toFile();
+
+    private final ResultWriter mWriter = new ResultWriter()
+            .forScenario(new ScenarioBuilder()
+                    .forClass(createTempFile("temp", "").getName()).build())
+            .withOutputDir(mTracingDirectory)
+            .setRunComplete();
+
+    private final TraceConfigs mTraceConfig = new TraceConfigs(
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false)
+    );
+
+    private PerfettoProtoLogImpl mProtoLog;
+    private Protolog.ProtoLogViewerConfig.Builder mViewerConfigBuilder;
+    private File mFile;
+
+    private ProtoLogViewerConfigReader mReader;
+
+    public PerfettoProtoLogImplTest() throws IOException {
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        final Context testContext = getInstrumentation().getContext();
+        mFile = testContext.getFileStreamPath("tracing_test.dat");
+        //noinspection ResultOfMethodCallIgnored
+        mFile.delete();
+
+        mViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder()
+                .addGroups(
+                        Protolog.ProtoLogViewerConfig.Group.newBuilder()
+                                .setId(1)
+                                .setName(TestProtoLogGroup.TEST_GROUP.toString())
+                                .setTag(TestProtoLogGroup.TEST_GROUP.getTag())
+                ).addMessages(
+                        Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(1)
+                                .setMessage("My Test Debug Log Message %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG)
+                                .setGroupId(1)
+                ).addMessages(
+                        Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(2)
+                                .setMessage("My Test Verbose Log Message %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE)
+                                .setGroupId(1)
+                ).addMessages(
+                        Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(3)
+                                .setMessage("My Test Warn Log Message %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WARN)
+                                .setGroupId(1)
+                ).addMessages(
+                        Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(4)
+                                .setMessage("My Test Error Log Message %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_ERROR)
+                                .setGroupId(1)
+                ).addMessages(
+                        Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(5)
+                                .setMessage("My Test WTF Log Message %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WTF)
+                                .setGroupId(1)
+                );
+
+        ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock(
+                ViewerConfigInputStreamProvider.class);
+        Mockito.when(viewerConfigInputStreamProvider.getInputStream())
+                .thenAnswer(it -> new ProtoInputStream(mViewerConfigBuilder.build().toByteArray()));
+
+        mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
+        mProtoLog = new PerfettoProtoLogImpl(viewerConfigInputStreamProvider, mReader);
+    }
+
+    @After
+    public void tearDown() {
+        if (mFile != null) {
+            //noinspection ResultOfMethodCallIgnored
+            mFile.delete();
+        }
+        ProtoLogImpl.setSingleInstance(null);
+    }
+
+    @Test
+    public void isEnabled_returnsFalseByDefault() {
+        assertFalse(mProtoLog.isProtoEnabled());
+    }
+
+    @Test
+    public void isEnabled_returnsTrueAfterStart() {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+        try {
+            traceMonitor.start();
+            assertTrue(mProtoLog.isProtoEnabled());
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+    }
+
+    @Test
+    public void isEnabled_returnsFalseAfterStop() {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+        try {
+            traceMonitor.start();
+            assertTrue(mProtoLog.isProtoEnabled());
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        assertFalse(mProtoLog.isProtoEnabled());
+    }
+
+    @Test
+    public void defaultMode() throws IOException {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(false).build();
+        try {
+            traceMonitor.start();
+            // Shouldn't be logging anything except WTF unless explicitly requested in the group
+            // override.
+            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+        Truth.assertThat(protolog.messages).hasSize(1);
+        Truth.assertThat(protolog.messages.getFirst().getLevel()).isEqualTo(LogLevel.WTF);
+    }
+
+    @Test
+    public void respectsOverrideConfigs_defaultMode() throws IOException {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(true,
+                        List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride(
+                                TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, true)))
+                        .build();
+        try {
+            traceMonitor.start();
+            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+        Truth.assertThat(protolog.messages).hasSize(5);
+        Truth.assertThat(protolog.messages.get(0).getLevel()).isEqualTo(LogLevel.DEBUG);
+        Truth.assertThat(protolog.messages.get(1).getLevel()).isEqualTo(LogLevel.VERBOSE);
+        Truth.assertThat(protolog.messages.get(2).getLevel()).isEqualTo(LogLevel.WARN);
+        Truth.assertThat(protolog.messages.get(3).getLevel()).isEqualTo(LogLevel.ERROR);
+        Truth.assertThat(protolog.messages.get(4).getLevel()).isEqualTo(LogLevel.WTF);
+    }
+
+    @Test
+    public void respectsOverrideConfigs_allEnabledMode() throws IOException {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(true,
+                        List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride(
+                                TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, false)))
+                        .build();
+        try {
+            traceMonitor.start();
+            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+        Truth.assertThat(protolog.messages).hasSize(3);
+        Truth.assertThat(protolog.messages.get(0).getLevel()).isEqualTo(LogLevel.WARN);
+        Truth.assertThat(protolog.messages.get(1).getLevel()).isEqualTo(LogLevel.ERROR);
+        Truth.assertThat(protolog.messages.get(2).getLevel()).isEqualTo(LogLevel.WTF);
+    }
+
+    @Test
+    public void respectsAllEnabledMode() throws IOException {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(true, List.of())
+                        .build();
+        try {
+            traceMonitor.start();
+            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+            mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5,
+                    LogDataType.BOOLEAN, null, new Object[]{true});
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+        Truth.assertThat(protolog.messages).hasSize(5);
+        Truth.assertThat(protolog.messages.get(0).getLevel()).isEqualTo(LogLevel.DEBUG);
+        Truth.assertThat(protolog.messages.get(1).getLevel()).isEqualTo(LogLevel.VERBOSE);
+        Truth.assertThat(protolog.messages.get(2).getLevel()).isEqualTo(LogLevel.WARN);
+        Truth.assertThat(protolog.messages.get(3).getLevel()).isEqualTo(LogLevel.ERROR);
+        Truth.assertThat(protolog.messages.get(4).getLevel()).isEqualTo(LogLevel.WTF);
+    }
+
+    @Test
+    public void log_logcatEnabledExternalMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f");
+        PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+                new Object[]{true, 10000, 30000, "test", 0.000003});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO),
+                eq("test true 10000 % 0x7530 test 3.0E-6"));
+        verify(mReader).getViewerString(eq(1234L));
+    }
+
+    @Test
+    public void log_logcatEnabledInvalidMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f");
+        PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+                new Object[]{true, 10000, 0.0001, 0.00002, "test"});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO),
+                eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test"));
+        verify(mReader).getViewerString(eq(1234L));
+    }
+
+    @Test
+    public void log_logcatEnabledInlineMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+        PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+                new Object[]{5});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO), eq("test 5"));
+        verify(mReader, never()).getViewerString(anyLong());
+    }
+
+    @Test
+    public void log_logcatEnabledNoMessage() {
+        when(mReader.getViewerString(anyLong())).thenReturn(null);
+        PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
+                new Object[]{5});
+
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5"));
+        verify(mReader).getViewerString(eq(1234L));
+    }
+
+    @Test
+    public void log_logcatDisabled() {
+        when(mReader.getViewerString(anyLong())).thenReturn("test %d");
+        PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
+
+        implSpy.log(
+                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
+                new Object[]{5});
+
+        verify(implSpy, never()).passToLogcat(any(), any(), any());
+        verify(mReader, never()).getViewerString(anyLong());
+    }
+
+    @Test
+    public void log_protoEnabled() throws Exception {
+        final long messageHash = addMessageToConfig(
+                ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_INFO,
+                "My test message :: %s, %d, %o, %x, %f, %e, %g, %b");
+
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+        long before;
+        long after;
+        try {
+            traceMonitor.start();
+            assertTrue(mProtoLog.isProtoEnabled());
+
+            before = SystemClock.elapsedRealtimeNanos();
+            mProtoLog.log(
+                    LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
+                    0b1110101001010100, null,
+                    new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
+            after = SystemClock.elapsedRealtimeNanos();
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+        Truth.assertThat(protolog.messages).hasSize(1);
+        Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos())
+                .isAtLeast(before);
+        Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos())
+                .isAtMost(after);
+        Truth.assertThat(protolog.messages.getFirst().getMessage())
+                .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, 5.000000e-01, 0.6, true");
+    }
+
+    private long addMessageToConfig(ProtologCommon.ProtoLogLevel logLevel, String message) {
+        final long messageId = new Random().nextLong();
+        mViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                .setMessageId(messageId)
+                .setMessage(message)
+                .setLevel(logLevel)
+                .setGroupId(1)
+        );
+
+        return messageId;
+    }
+
+    @Test
+    public void log_invalidParamsMask() {
+        final long messageHash = addMessageToConfig(
+                ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_INFO,
+                "My test message :: %s, %d, %f, %b");
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog().build();
+        long before;
+        long after;
+        try {
+            traceMonitor.start();
+            before = SystemClock.elapsedRealtimeNanos();
+            mProtoLog.log(
+                    LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
+                    0b01100100, null,
+                    new Object[]{"test", 1, 0.1, true});
+            after = SystemClock.elapsedRealtimeNanos();
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        assertThrows(IllegalStateException.class, reader::readProtoLogTrace);
+    }
+
+    @Test
+    public void log_protoDisabled() throws Exception {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(false).build();
+        try {
+            traceMonitor.start();
+            mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1,
+                    0b11, null, new Object[]{true});
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+        Truth.assertThat(protolog.messages).isEmpty();
+    }
+
+    @Test
+    public void stackTraceTrimmed() throws IOException {
+        PerfettoTraceMonitor traceMonitor =
+                PerfettoTraceMonitor.newBuilder().enableProtoLog(true,
+                        List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride(
+                                TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, true)))
+                        .build();
+        try {
+            traceMonitor.start();
+
+            ProtoLogImpl.setSingleInstance(mProtoLog);
+            ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1,
+                    0b11, null, true);
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final ProtoLogTrace protolog = reader.readProtoLogTrace();
+
+        Truth.assertThat(protolog.messages).hasSize(1);
+        String stacktrace = protolog.messages.getFirst().getStacktrace();
+        Truth.assertThat(stacktrace)
+                .doesNotContain(PerfettoProtoLogImpl.class.getSimpleName() + ".java");
+        Truth.assertThat(stacktrace).doesNotContain(DataSource.class.getSimpleName() + ".java");
+        Truth.assertThat(stacktrace)
+                .doesNotContain(ProtoLogImpl.class.getSimpleName() + ".java");
+        Truth.assertThat(stacktrace).contains(PerfettoProtoLogImplTest.class.getSimpleName());
+        Truth.assertThat(stacktrace).contains("stackTraceTrimmed");
+    }
+
+    private enum TestProtoLogGroup implements IProtoLogGroup {
+        TEST_GROUP(true, true, false, "TEST_TAG");
+
+        private final boolean mEnabled;
+        private volatile boolean mLogToProto;
+        private volatile boolean mLogToLogcat;
+        private final String mTag;
+
+        /**
+         * @param enabled     set to false to exclude all log statements for this group from
+         *                    compilation,
+         *                    they will not be available in runtime.
+         * @param logToProto  enable binary logging for the group
+         * @param logToLogcat enable text logging for the group
+         * @param tag         name of the source of the logged message
+         */
+        TestProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) {
+            this.mEnabled = enabled;
+            this.mLogToProto = logToProto;
+            this.mLogToLogcat = logToLogcat;
+            this.mTag = tag;
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return mEnabled;
+        }
+
+        @Override
+        public boolean isLogToProto() {
+            return mLogToProto;
+        }
+
+        @Override
+        public boolean isLogToLogcat() {
+            return mLogToLogcat;
+        }
+
+        @Override
+        public boolean isLogToAny() {
+            return mLogToLogcat || mLogToProto;
+        }
+
+        @Override
+        public String getTag() {
+            return mTag;
+        }
+
+        @Override
+        public void setLogToProto(boolean logToProto) {
+            this.mLogToProto = logToProto;
+        }
+
+        @Override
+        public void setLogToLogcat(boolean logToLogcat) {
+            this.mLogToLogcat = logToLogcat;
+        }
+
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
index 7deb8c73d1fcecb99134e2549978745c52319027..4267c2c127aeaed0f296ec19433028e4c36b0839 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java
@@ -16,49 +16,23 @@
 
 package com.android.internal.protolog;
 
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.internal.protolog.ProtoLogImpl.PROTOLOG_VERSION;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
-import android.content.Context;
-import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
-import android.util.proto.ProtoInputStream;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogLevel;
 
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.util.LinkedList;
 
 /**
  * Test class for {@link ProtoLogImpl}.
@@ -68,336 +42,78 @@ import java.util.LinkedList;
 @Presubmit
 @RunWith(JUnit4.class)
 public class ProtoLogImplTest {
-
-    private static final byte[] MAGIC_HEADER = new byte[]{
-            0x9, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x4c, 0x4f, 0x47
-    };
-
-    private ProtoLogImpl mProtoLog;
-    private File mFile;
-
-    @Mock
-    private ProtoLogViewerConfigReader mReader;
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        final Context testContext = getInstrumentation().getContext();
-        mFile = testContext.getFileStreamPath("tracing_test.dat");
-        //noinspection ResultOfMethodCallIgnored
-        mFile.delete();
-        mProtoLog = new ProtoLogImpl(mFile, 1024 * 1024, mReader, 1024);
-    }
-
     @After
     public void tearDown() {
-        if (mFile != null) {
-            //noinspection ResultOfMethodCallIgnored
-            mFile.delete();
-        }
         ProtoLogImpl.setSingleInstance(null);
     }
 
-    @Test
-    public void isEnabled_returnsFalseByDefault() {
-        assertFalse(mProtoLog.isProtoEnabled());
-    }
-
-    @Test
-    public void isEnabled_returnsTrueAfterStart() {
-        mProtoLog.startProtoLog(mock(PrintWriter.class));
-        assertTrue(mProtoLog.isProtoEnabled());
-    }
-
-    @Test
-    public void isEnabled_returnsFalseAfterStop() {
-        mProtoLog.startProtoLog(mock(PrintWriter.class));
-        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
-        assertFalse(mProtoLog.isProtoEnabled());
-    }
-
-    @Test
-    public void logFile_startsWithMagicHeader() throws Exception {
-        mProtoLog.startProtoLog(mock(PrintWriter.class));
-        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
-
-        assertTrue("Log file should exist", mFile.exists());
-
-        byte[] header = new byte[MAGIC_HEADER.length];
-        try (InputStream is = new FileInputStream(mFile)) {
-            assertEquals(MAGIC_HEADER.length, is.read(header));
-            assertArrayEquals(MAGIC_HEADER, header);
-        }
-    }
-
     @Test
     public void getSingleInstance() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         assertSame(mockedProtoLog, ProtoLogImpl.getSingleInstance());
     }
 
     @Test
     public void d_logCalled() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
-        verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.DEBUG), eq(
+        verify(mockedProtoLog).log(eq(LogLevel.DEBUG), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void v_logCalled() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
-        verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.VERBOSE), eq(
+        verify(mockedProtoLog).log(eq(LogLevel.VERBOSE), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void i_logCalled() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
-        verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.INFO), eq(
+        verify(mockedProtoLog).log(eq(LogLevel.INFO), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void w_logCalled() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234,
                 4321, "test %d");
-        verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.WARN), eq(
+        verify(mockedProtoLog).log(eq(LogLevel.WARN), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void e_logCalled() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d");
-        verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.ERROR), eq(
+        verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     @Test
     public void wtf_logCalled() {
-        ProtoLogImpl mockedProtoLog = mock(ProtoLogImpl.class);
+        IProtoLog mockedProtoLog = mock(IProtoLog.class);
         ProtoLogImpl.setSingleInstance(mockedProtoLog);
         ProtoLogImpl.wtf(TestProtoLogGroup.TEST_GROUP,
                 1234, 4321, "test %d");
-        verify(mockedProtoLog).log(eq(ProtoLogImpl.LogLevel.WTF), eq(
+        verify(mockedProtoLog).log(eq(LogLevel.WTF), eq(
                 TestProtoLogGroup.TEST_GROUP),
-                eq(1234), eq(4321), eq("test %d"), eq(new Object[]{}));
-    }
-
-    @Test
-    public void log_logcatEnabledExternalMessage() {
-        when(mReader.getViewerString(anyInt())).thenReturn("test %b %d %% 0x%x %s %f");
-        ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
-        implSpy.log(
-                ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
-                new Object[]{true, 10000, 30000, "test", 0.000003});
-
-        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                ProtoLogImpl.LogLevel.INFO),
-                eq("test true 10000 % 0x7530 test 3.0E-6"));
-        verify(mReader).getViewerString(eq(1234));
-    }
-
-    @Test
-    public void log_logcatEnabledInvalidMessage() {
-        when(mReader.getViewerString(anyInt())).thenReturn("test %b %d %% %x %s %f");
-        ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
-        implSpy.log(
-                ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
-                new Object[]{true, 10000, 0.0001, 0.00002, "test"});
-
-        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                ProtoLogImpl.LogLevel.INFO),
-                eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test"));
-        verify(mReader).getViewerString(eq(1234));
-    }
-
-    @Test
-    public void log_logcatEnabledInlineMessage() {
-        when(mReader.getViewerString(anyInt())).thenReturn("test %d");
-        ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
-        implSpy.log(
-                ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
-                new Object[]{5});
-
-        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                ProtoLogImpl.LogLevel.INFO), eq("test 5"));
-        verify(mReader, never()).getViewerString(anyInt());
-    }
-
-    @Test
-    public void log_logcatEnabledNoMessage() {
-        when(mReader.getViewerString(anyInt())).thenReturn(null);
-        ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
-        implSpy.log(
-                ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null,
-                new Object[]{5});
-
-        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                ProtoLogImpl.LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5"));
-        verify(mReader).getViewerString(eq(1234));
-    }
-
-    @Test
-    public void log_logcatDisabled() {
-        when(mReader.getViewerString(anyInt())).thenReturn("test %d");
-        ProtoLogImpl implSpy = Mockito.spy(mProtoLog);
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-
-        implSpy.log(
-                ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d",
-                new Object[]{5});
-
-        verify(implSpy, never()).passToLogcat(any(), any(), any());
-        verify(mReader, never()).getViewerString(anyInt());
-    }
-
-    private static class ProtoLogData {
-        Integer mMessageHash = null;
-        Long mElapsedTime = null;
-        LinkedList<String> mStrParams = new LinkedList<>();
-        LinkedList<Long> mSint64Params = new LinkedList<>();
-        LinkedList<Double> mDoubleParams = new LinkedList<>();
-        LinkedList<Boolean> mBooleanParams = new LinkedList<>();
-    }
-
-    private ProtoLogData readProtoLogSingle(ProtoInputStream ip) throws IOException {
-        while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-            if (ip.getFieldNumber() == (int) ProtoLogFileProto.VERSION) {
-                assertEquals(PROTOLOG_VERSION, ip.readString(ProtoLogFileProto.VERSION));
-                continue;
-            }
-            if (ip.getFieldNumber() != (int) ProtoLogFileProto.LOG) {
-                continue;
-            }
-            long token = ip.start(ProtoLogFileProto.LOG);
-            ProtoLogData data = new ProtoLogData();
-            while (ip.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-                switch (ip.getFieldNumber()) {
-                    case (int) ProtoLogMessage.MESSAGE_HASH: {
-                        data.mMessageHash = ip.readInt(ProtoLogMessage.MESSAGE_HASH);
-                        break;
-                    }
-                    case (int) ProtoLogMessage.ELAPSED_REALTIME_NANOS: {
-                        data.mElapsedTime = ip.readLong(ProtoLogMessage.ELAPSED_REALTIME_NANOS);
-                        break;
-                    }
-                    case (int) ProtoLogMessage.STR_PARAMS: {
-                        data.mStrParams.add(ip.readString(ProtoLogMessage.STR_PARAMS));
-                        break;
-                    }
-                    case (int) ProtoLogMessage.SINT64_PARAMS: {
-                        data.mSint64Params.add(ip.readLong(ProtoLogMessage.SINT64_PARAMS));
-                        break;
-                    }
-                    case (int) ProtoLogMessage.DOUBLE_PARAMS: {
-                        data.mDoubleParams.add(ip.readDouble(ProtoLogMessage.DOUBLE_PARAMS));
-                        break;
-                    }
-                    case (int) ProtoLogMessage.BOOLEAN_PARAMS: {
-                        data.mBooleanParams.add(ip.readBoolean(ProtoLogMessage.BOOLEAN_PARAMS));
-                        break;
-                    }
-                }
-            }
-            ip.end(token);
-            return data;
-        }
-        return null;
-    }
-
-    @Test
-    public void log_protoEnabled() throws Exception {
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
-        mProtoLog.startProtoLog(mock(PrintWriter.class));
-        long before = SystemClock.elapsedRealtimeNanos();
-        mProtoLog.log(
-                ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
-                0b1110101001010100, null,
-                new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
-        long after = SystemClock.elapsedRealtimeNanos();
-        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
-        try (InputStream is = new FileInputStream(mFile)) {
-            ProtoInputStream ip = new ProtoInputStream(is);
-            ProtoLogData data = readProtoLogSingle(ip);
-            assertNotNull(data);
-            assertEquals(1234, data.mMessageHash.longValue());
-            assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
-            assertArrayEquals(new String[]{"test"}, data.mStrParams.toArray());
-            assertArrayEquals(new Long[]{1L, 2L, 3L}, data.mSint64Params.toArray());
-            assertArrayEquals(new Double[]{0.4, 0.5, 0.6}, data.mDoubleParams.toArray());
-            assertArrayEquals(new Boolean[]{true}, data.mBooleanParams.toArray());
-        }
-    }
-
-    @Test
-    public void log_invalidParamsMask() throws Exception {
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(true);
-        mProtoLog.startProtoLog(mock(PrintWriter.class));
-        long before = SystemClock.elapsedRealtimeNanos();
-        mProtoLog.log(
-                ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
-                0b01100100, null,
-                new Object[]{"test", 1, 0.1, true});
-        long after = SystemClock.elapsedRealtimeNanos();
-        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
-        try (InputStream is = new FileInputStream(mFile)) {
-            ProtoInputStream ip = new ProtoInputStream(is);
-            ProtoLogData data = readProtoLogSingle(ip);
-            assertNotNull(data);
-            assertEquals(1234, data.mMessageHash.longValue());
-            assertTrue(before <= data.mElapsedTime && data.mElapsedTime <= after);
-            assertArrayEquals(new String[]{"test", "(INVALID PARAMS_MASK) true"},
-                    data.mStrParams.toArray());
-            assertArrayEquals(new Long[]{1L}, data.mSint64Params.toArray());
-            assertArrayEquals(new Double[]{0.1}, data.mDoubleParams.toArray());
-            assertArrayEquals(new Boolean[]{}, data.mBooleanParams.toArray());
-        }
-    }
-
-    @Test
-    public void log_protoDisabled() throws Exception {
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false);
-        TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
-        mProtoLog.startProtoLog(mock(PrintWriter.class));
-        mProtoLog.log(ProtoLogImpl.LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234,
-                0b11, null, new Object[]{true});
-        mProtoLog.stopProtoLog(mock(PrintWriter.class), true);
-        try (InputStream is = new FileInputStream(mFile)) {
-            ProtoInputStream ip = new ProtoInputStream(is);
-            ProtoLogData data = readProtoLogSingle(ip);
-            assertNull(data);
-        }
+                eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{}));
     }
 
     private enum TestProtoLogGroup implements IProtoLogGroup {
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
index ae5021638745932a262407247baf865fa961a859..dbd85d38b7f28ddf9fec7f2bc2eafca006c964c6 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
@@ -72,8 +72,8 @@ public class ProtoLogViewerConfigReaderTest {
             + "}\n";
 
 
-    private ProtoLogViewerConfigReader
-            mConfig = new ProtoLogViewerConfigReader();
+    private LegacyProtoLogViewerConfigReader
+            mConfig = new LegacyProtoLogViewerConfigReader();
     private File mTestViewerConfig;
 
     @Before
@@ -98,7 +98,7 @@ public class ProtoLogViewerConfigReaderTest {
 
     @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 @@ public class ProtoLogViewerConfigReaderTest {
 
     @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/Android.bp b/tools/protologtool/Android.bp
index 46745e995f645e354299edb78a859fa689cb2924..8fbc3e8a78dba676aebce54fb9f3262b3e27b713 100644
--- a/tools/protologtool/Android.bp
+++ b/tools/protologtool/Android.bp
@@ -11,12 +11,13 @@ java_library_host {
     name: "protologtool-lib",
     srcs: [
         "src/com/android/protolog/tool/**/*.kt",
-        ":protolog-common-no-android-src",
+        ":protolog-common-src",
     ],
     static_libs: [
         "javaparser",
         "platformprotos",
         "jsonlib",
+        "perfetto_trace-full",
     ],
 }
 
@@ -42,5 +43,6 @@ java_test_host {
         "junit",
         "mockito",
         "objenesis",
+        "truth",
     ],
 }
diff --git a/tools/protologtool/README.md b/tools/protologtool/README.md
index ba639570f88cc0eb2e31c1d4366ccfba8c07b9c1..24a4861c63a296a9a229da65ea5034199181f715 100644
--- a/tools/protologtool/README.md
+++ b/tools/protologtool/README.md
@@ -8,11 +8,13 @@ ProtoLogTool incorporates three different modes of operation:
 
 ### Code transformation
 
-Command: `protologtool transform-protolog-calls 
-    --protolog-class <protolog class name> 
-    --protolog-impl-class <protolog implementation class name>
+Command: `protologtool transform-protolog-calls
+    --protolog-class <protolog class name>
     --loggroups-class <protolog groups class name>
     --loggroups-jar <config jar path>
+    --viewer-config-file-path <protobuf viewer config file path>
+    --legacy-viewer-config-file-path <legacy json.gz viewer config file path>
+    --legacy-output-file-path <.winscope file path to write the legacy trace to>
     --output-srcjar <output.srcjar>
     [<input.java>]`
 
@@ -44,10 +46,11 @@ jar file (config.jar).
 ### Viewer config generation
 
 Command: `generate-viewer-config
-    --protolog-class <protolog class name> 
+    --protolog-class <protolog class name>
     --loggroups-class <protolog groups class name>
     --loggroups-jar <config jar path>
-    --viewer-conf <viewer.json>
+    --viewer-config-type <proto|json>
+    --viewer-config <viewer.json>
     [<input.java>]`
 
 This command is similar in it's syntax to the previous one, only instead of creating a processed source jar
@@ -74,7 +77,7 @@ it writes a viewer configuration file with following schema:
 
 ### Binary log viewing
 
-Command: `read-log --viewer-conf <viewer.json> <wm_log.pb>`
+Command: `read-log --viewer-config <viewer.json> <wm_log.pb>`
 
 Reads the binary ProtoLog log file and outputs a human-readable LogCat-like text log.
 
diff --git a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
index 07c6fd32831856eb7334b7f212acb2d2faabeabd..3d1dec2e07248e7fbde5881284647bfa3a3db13b 100644
--- a/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/CodeUtils.kt
@@ -22,15 +22,21 @@ import com.github.javaparser.ast.ImportDeclaration
 import com.github.javaparser.ast.expr.BinaryExpr
 import com.github.javaparser.ast.expr.Expression
 import com.github.javaparser.ast.expr.StringLiteralExpr
+import java.util.UUID
 
 object CodeUtils {
     /**
      * Returns a stable hash of a string.
      * We reimplement String::hashCode() for readability reasons.
      */
-    fun hash(position: String, messageString: String, logLevel: LogLevel, logGroup: LogGroup): Int {
-        return (position + messageString + logLevel.name + logGroup.name)
-                .map { c -> c.code }.reduce { h, c -> h * 31 + c }
+    fun hash(
+        position: String,
+        messageString: String,
+        logLevel: LogLevel,
+        logGroup: LogGroup
+    ): Long {
+        val fullStringIdentifier = position + messageString + logLevel.name + logGroup.name
+        return UUID.nameUUIDFromBytes(fullStringIdentifier.toByteArray()).mostSignificantBits
     }
 
     fun checkWildcardStaticImported(code: CompilationUnit, className: String, fileName: String) {
diff --git a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
index bfbbf7a32c2276098a7adbe6a3e03c22e2141a97..a3591558bd67227468fe7ce65952173c28099d49 100644
--- a/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
+++ b/tools/protologtool/src/com/android/protolog/tool/CommandOptions.kt
@@ -26,32 +26,35 @@ class CommandOptions(args: Array<String>) {
         private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD)
 
         private const val PROTOLOG_CLASS_PARAM = "--protolog-class"
-        private const val PROTOLOGIMPL_CLASS_PARAM = "--protolog-impl-class"
-        private const val PROTOLOGCACHE_CLASS_PARAM = "--protolog-cache-class"
         private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class"
         private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar"
-        private const val VIEWER_CONFIG_JSON_PARAM = "--viewer-conf"
+        private const val VIEWER_CONFIG_PARAM = "--viewer-config"
+        private const val VIEWER_CONFIG_TYPE_PARAM = "--viewer-config-type"
         private const val OUTPUT_SOURCE_JAR_PARAM = "--output-srcjar"
-        private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGIMPL_CLASS_PARAM,
-                PROTOLOGCACHE_CLASS_PARAM, PROTOLOGGROUP_CLASS_PARAM, PROTOLOGGROUP_JAR_PARAM,
-                VIEWER_CONFIG_JSON_PARAM, OUTPUT_SOURCE_JAR_PARAM)
+        private const val VIEWER_CONFIG_FILE_PATH_PARAM = "--viewer-config-file-path"
+        // TODO(b/324128613): Remove these legacy options once we fully flip the Perfetto protolog flag
+        private const val LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM = "--legacy-viewer-config-file-path"
+        private const val LEGACY_OUTPUT_FILE_PATH = "--legacy-output-file-path"
+        private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGGROUP_CLASS_PARAM,
+            PROTOLOGGROUP_JAR_PARAM, VIEWER_CONFIG_PARAM, VIEWER_CONFIG_TYPE_PARAM,
+            OUTPUT_SOURCE_JAR_PARAM, VIEWER_CONFIG_FILE_PATH_PARAM,
+            LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, LEGACY_OUTPUT_FILE_PATH)
 
         val USAGE = """
             Usage: ${Constants.NAME} <command> [<args>]
             Available commands:
 
-            $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGIMPL_CLASS_PARAM
-                <class name> $PROTOLOGCACHE_CLASS_PARAM
-                <class name> $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM
-                <config.jar> $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>]
+            $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name>
+                $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar>
+                $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>]
             - processes java files replacing stub calls with logging code.
 
-            $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name> $PROTOLOGGROUP_CLASS_PARAM
-                <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar> $VIEWER_CONFIG_JSON_PARAM
-                <viewer.json> [<input.java>]
+            $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name>
+                $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar>
+                $VIEWER_CONFIG_PARAM <viewer.json|viewer.pb> [<input.java>]
             - creates viewer config file from given java files.
 
-            $READ_LOG_CMD $VIEWER_CONFIG_JSON_PARAM <viewer.json> <wm_log.pb>
+            $READ_LOG_CMD $VIEWER_CONFIG_PARAM <viewer.json|viewer.pb> <wm_log.pb>
             - translates a binary log to a readable format.
         """.trimIndent()
 
@@ -69,6 +72,13 @@ class CommandOptions(args: Array<String>) {
             return params.getValue(paramName)
         }
 
+        private fun getOptionalParam(paramName: String, params: Map<String, String>): String? {
+            if (!params.containsKey(paramName)) {
+                return null
+            }
+            return params.getValue(paramName)
+        }
+
         private fun validateNotSpecified(paramName: String, params: Map<String, String>): String {
             if (params.containsKey(paramName)) {
                 throw InvalidCommandException("Unsupported param $paramName")
@@ -90,9 +100,43 @@ class CommandOptions(args: Array<String>) {
             return name
         }
 
-        private fun validateJSONName(name: String): String {
-            if (!name.endsWith(".json")) {
-                throw InvalidCommandException("Json file required, got $name instead")
+        private fun validateViewerConfigFilePath(name: String): String {
+            if (!name.endsWith(".pb")) {
+                throw InvalidCommandException("Proto file (ending with .pb) required, " +
+                        "got $name instead")
+            }
+            return name
+        }
+
+        private fun validateLegacyViewerConfigFilePath(name: String): String {
+            if (!name.endsWith(".json.gz")) {
+                throw InvalidCommandException("GZiped Json file (ending with .json.gz) required, " +
+                        "got $name instead")
+            }
+            return name
+        }
+
+        private fun validateOutputFilePath(name: String): String {
+            if (!name.endsWith(".winscope")) {
+                throw InvalidCommandException("Winscope file (ending with .winscope) required, " +
+                        "got $name instead")
+            }
+            return name
+        }
+
+        private fun validateConfigFileName(name: String): String {
+            if (!name.endsWith(".json") && !name.endsWith(".pb")) {
+                throw InvalidCommandException("Json file (ending with .json) or proto file " +
+                        "(ending with .pb) required, got $name instead")
+            }
+            return name
+        }
+
+        private fun validateConfigType(name: String): String {
+            val validType = listOf("json", "proto")
+            if (!validType.contains(name)) {
+                throw InvalidCommandException("Unexpected config file type. " +
+                        "Expected on of [${validType.joinToString()}], but got $name")
             }
             return name
         }
@@ -102,8 +146,8 @@ class CommandOptions(args: Array<String>) {
                 throw InvalidCommandException("No java source input files")
             }
             list.forEach { name ->
-                if (!name.endsWith(".java")) {
-                    throw InvalidCommandException("Not a java source file $name")
+                if (!name.endsWith(".java") && !name.endsWith(".kt")) {
+                    throw InvalidCommandException("Not a java or kotlin source file $name")
                 }
             }
             return list
@@ -122,12 +166,14 @@ class CommandOptions(args: Array<String>) {
 
     val protoLogClassNameArg: String
     val protoLogGroupsClassNameArg: String
-    val protoLogImplClassNameArg: String
-    val protoLogCacheClassNameArg: String
     val protoLogGroupsJarArg: String
-    val viewerConfigJsonArg: String
+    val viewerConfigFileNameArg: String
+    val viewerConfigTypeArg: String
     val outputSourceJarArg: String
     val logProtofileArg: String
+    val viewerConfigFilePathArg: String
+    val legacyViewerConfigFilePathArg: String?
+    val legacyOutputFilePath: String?
     val javaSourceArgs: List<String>
     val command: String
 
@@ -169,38 +215,55 @@ class CommandOptions(args: Array<String>) {
         when (command) {
             TRANSFORM_CALLS_CMD -> {
                 protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
-                protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
-                        params))
-                protoLogImplClassNameArg = validateClassName(getParam(PROTOLOGIMPL_CLASS_PARAM,
-                        params))
-                protoLogCacheClassNameArg = validateClassName(getParam(PROTOLOGCACHE_CLASS_PARAM,
-                        params))
+                protoLogGroupsClassNameArg =
+                    validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, params))
                 protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
-                viewerConfigJsonArg = validateNotSpecified(VIEWER_CONFIG_JSON_PARAM, params)
+                viewerConfigFileNameArg = validateNotSpecified(VIEWER_CONFIG_PARAM, params)
+                viewerConfigTypeArg = validateNotSpecified(VIEWER_CONFIG_TYPE_PARAM, params)
                 outputSourceJarArg = validateSrcJarName(getParam(OUTPUT_SOURCE_JAR_PARAM, params))
+                viewerConfigFilePathArg = validateViewerConfigFilePath(
+                    getParam(VIEWER_CONFIG_FILE_PATH_PARAM, params))
+                legacyViewerConfigFilePathArg =
+                    getOptionalParam(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)?.let {
+                        validateLegacyViewerConfigFilePath(it)
+                    }
+                legacyOutputFilePath =
+                    getOptionalParam(LEGACY_OUTPUT_FILE_PATH, params)?.let {
+                        validateOutputFilePath(it)
+                    }
                 javaSourceArgs = validateJavaInputList(inputFiles)
                 logProtofileArg = ""
             }
             GENERATE_CONFIG_CMD -> {
                 protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
-                protoLogGroupsClassNameArg = validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM,
-                        params))
-                protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
-                protoLogCacheClassNameArg = validateNotSpecified(PROTOLOGCACHE_CLASS_PARAM, params)
+                protoLogGroupsClassNameArg =
+                    validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, params))
                 protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
-                viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+                viewerConfigFileNameArg =
+                    validateConfigFileName(getParam(VIEWER_CONFIG_PARAM, params))
+                viewerConfigTypeArg = validateConfigType(getParam(VIEWER_CONFIG_TYPE_PARAM, params))
                 outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+                viewerConfigFilePathArg =
+                    validateNotSpecified(VIEWER_CONFIG_FILE_PATH_PARAM, params)
+                legacyViewerConfigFilePathArg =
+                    validateNotSpecified(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)
+                legacyOutputFilePath = validateNotSpecified(LEGACY_OUTPUT_FILE_PATH, params)
                 javaSourceArgs = validateJavaInputList(inputFiles)
                 logProtofileArg = ""
             }
             READ_LOG_CMD -> {
                 protoLogClassNameArg = validateNotSpecified(PROTOLOG_CLASS_PARAM, params)
                 protoLogGroupsClassNameArg = validateNotSpecified(PROTOLOGGROUP_CLASS_PARAM, params)
-                protoLogImplClassNameArg = validateNotSpecified(PROTOLOGIMPL_CLASS_PARAM, params)
-                protoLogCacheClassNameArg = validateNotSpecified(PROTOLOGCACHE_CLASS_PARAM, params)
                 protoLogGroupsJarArg = validateNotSpecified(PROTOLOGGROUP_JAR_PARAM, params)
-                viewerConfigJsonArg = validateJSONName(getParam(VIEWER_CONFIG_JSON_PARAM, params))
+                viewerConfigFileNameArg =
+                    validateConfigFileName(getParam(VIEWER_CONFIG_PARAM, params))
+                viewerConfigTypeArg = validateNotSpecified(VIEWER_CONFIG_TYPE_PARAM, params)
                 outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
+                viewerConfigFilePathArg =
+                    validateNotSpecified(VIEWER_CONFIG_FILE_PATH_PARAM, params)
+                legacyViewerConfigFilePathArg =
+                    validateNotSpecified(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)
+                legacyOutputFilePath = validateNotSpecified(LEGACY_OUTPUT_FILE_PATH, params)
                 javaSourceArgs = listOf()
                 logProtofileArg = validateLogInputList(inputFiles)
             }
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 0000000000000000000000000000000000000000..fda6351361bf91f3e206905584b8a7ac84b3fc24
--- /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 9a76a6f84c2b1341796688ab33f2a17545b1db42..47724b7a9e1d3792f8c89a31f2afa799bdbfae61 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 0000000000000000000000000000000000000000..1087ae6ee41d39fafd88a9ab3ab26f3e1d491737
--- /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 ce856cd4961452022ed758fc417ae35652eae8de..1381847c258f4de121a43e1e21338d6ab1fcb23d 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.ExecutorService
 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 @@ object ProtoLogTool {
     }
 
     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 @@ object ProtoLogTool {
             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 @@ ${updates.replaceIndent("                    ")}
             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 @@ ${updates.replaceIndent("                    ")}
                     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 @@ ${updates.replaceIndent("                    ")}
                     }
                 }
             }.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 @@ ${updates.replaceIndent("                    ")}
     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 b50f357c57ec6d558b4b30d7711534f4a26970bc..2b7164191dd0fff935f0d6e1b41ffd54d07d5607 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.StaticJavaParser
 import com.github.javaparser.ast.CompilationUnit
 import com.github.javaparser.ast.NodeList
 import com.github.javaparser.ast.body.VariableDeclarator
-import com.github.javaparser.ast.expr.BooleanLiteralExpr
 import com.github.javaparser.ast.expr.CastExpr
 import com.github.javaparser.ast.expr.Expression
 import com.github.javaparser.ast.expr.FieldAccessExpr
 import com.github.javaparser.ast.expr.IntegerLiteralExpr
+import com.github.javaparser.ast.expr.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.TypeExpr
 import com.github.javaparser.ast.expr.VariableDeclarationExpr
 import com.github.javaparser.ast.stmt.BlockStmt
 import com.github.javaparser.ast.stmt.ExpressionStmt
-import com.github.javaparser.ast.stmt.IfStmt
 import com.github.javaparser.ast.type.ArrayType
 import com.github.javaparser.ast.type.ClassOrInterfaceType
 import com.github.javaparser.ast.type.PrimitiveType
@@ -45,15 +44,59 @@ import com.github.javaparser.printer.PrettyPrinterConfiguration
 
 class SourceTransformer(
     protoLogImplClassName: String,
-    protoLogCacheClassName: String,
     private val protoLogCallProcessor: ProtoLogCallProcessor
-) : ProtoLogCallVisitor {
-    override fun processCall(
-        call: MethodCallExpr,
-        messageString: String,
-        level: LogLevel,
-        group: LogGroup
-    ) {
+) {
+    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 @@ class SourceTransformer(
             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)
+    }
+
+    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)))
             }
-            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 ")
+            // 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 @@ class SourceTransformer(
         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 @@ class SourceTransformer(
             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 0d5d022959b2b53a09ec7fd50b3f11e08b98ffde..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..7714db212c9fedd8b30c9a93ff68acaaf86c70ff
--- /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 7278db0094e6c56bef1b080a23007ec710c174ec..58be3a3e04af0d219dee8b89569695a313ae7c2f 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 @@ open class ViewerConfigParser {
         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 @@ open class ViewerConfigParser {
 
     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 0000000000000000000000000000000000000000..cf0876a1f07237b3e014297ddc95481f97a4c8a2
--- /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/CodeUtilsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
index b08d859bad32ddc8a709e21ac6e5bfd55206e994..0cd02a5c5ce800fb3825fe1c53dd99d8afd4b6cc 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/CodeUtilsTest.kt
@@ -28,31 +28,31 @@ import org.junit.Test
 class CodeUtilsTest {
     @Test
     fun hash() {
-        assertEquals(-1259556708, CodeUtils.hash("Test.java:50", "test",
+        assertEquals(3883826472308915399, CodeUtils.hash("Test.java:50", "test",
                 LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
     }
 
     @Test
     fun hash_changeLocation() {
-        assertEquals(15793504, CodeUtils.hash("Test.java:10", "test2",
+        assertEquals(4125273133972468649, CodeUtils.hash("Test.java:10", "test2",
                 LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
     }
 
     @Test
     fun hash_changeLevel() {
-        assertEquals(-731772463, CodeUtils.hash("Test.java:50", "test",
+        assertEquals(2618535069521361990, CodeUtils.hash("Test.java:50", "test",
                 LogLevel.ERROR, LogGroup("test", true, true, "TAG")))
     }
 
     @Test
     fun hash_changeMessage() {
-        assertEquals(-2026343204, CodeUtils.hash("Test.java:50", "test2",
+        assertEquals(8907822592109789043, CodeUtils.hash("Test.java:50", "test2",
                 LogLevel.DEBUG, LogGroup("test", true, true, "TAG")))
     }
 
     @Test
     fun hash_changeGroup() {
-        assertEquals(1607870166, CodeUtils.hash("Test.java:50", "test2",
+        assertEquals(-1299517016176640015, CodeUtils.hash("Test.java:50", "test2",
                 LogLevel.DEBUG, LogGroup("test2", true, true, "TAG")))
     }
 
diff --git a/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
index 3cfbb435a764399632faf607cbfc724b535024ba..5ef2833080a278a90743de01cd3799c1c903c32d 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/CommandOptionsTest.kt
@@ -16,7 +16,9 @@
 
 package com.android.protolog.tool
 
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
 import org.junit.Test
 
 class CommandOptionsTest {
@@ -35,6 +37,10 @@ class CommandOptionsTest {
         private const val TEST_PROTOLOGGROUP_JAR = "out/soong/.intermediates/frameworks/base/" +
                 "services/core/services.core.wm.protologgroups/android_common/javac/" +
                 "services.core.wm.protologgroups.jar"
+        private const val TEST_VIEWER_CONFIG_FILE_PATH = "/some/viewer/config/file/path.pb"
+        private const val TEST_LEGACY_VIEWER_CONFIG_FILE_PATH =
+            "/some/viewer/config/file/path.json.gz"
+        private const val TEST_LEGACY_OUTPUT_FILE_PATH = "/some/output/file/path.winscope"
         private const val TEST_SRC_JAR = "out/soong/.temp/sbox175955373/" +
                 "services.core.wm.protolog.srcjar"
         private const val TEST_VIEWER_JSON = "out/soong/.temp/sbox175955373/" +
@@ -42,186 +48,263 @@ class CommandOptionsTest {
         private const val TEST_LOG = "./test_log.pb"
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun noCommand() {
-        CommandOptions(arrayOf())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(arrayOf())
+        }
+        assertThat(exception).hasMessageThat().contains("No command specified")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun invalidCommand() {
         val testLine = "invalid"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("Unknown command")
     }
 
     @Test
     fun transformClasses() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
         val cmd = CommandOptions(testLine.split(' ').toTypedArray())
         assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
         assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
-        assertEquals(TEST_PROTOLOGIMPL_CLASS, cmd.protoLogImplClassNameArg)
         assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
         assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+        assertEquals(TEST_VIEWER_CONFIG_FILE_PATH, cmd.viewerConfigFilePathArg)
+        assertEquals(TEST_LEGACY_VIEWER_CONFIG_FILE_PATH, cmd.legacyViewerConfigFilePathArg)
+        assertEquals(TEST_LEGACY_OUTPUT_FILE_PATH, cmd.legacyOutputFilePath)
         assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
         assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
     }
 
-    @Test(expected = InvalidCommandException::class)
-    fun transformClasses_noProtoLogClass() {
+    @Test
+    fun transformClasses_noViewerConfigFile() {
         val testLine = "transform-protolog-calls " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("--viewer-config-file-path")
     }
 
-    @Test(expected = InvalidCommandException::class)
-    fun transformClasses_noProtoLogImplClass() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+    @Test
+    fun transformClasses_noLegacyViewerConfigFile() {
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+        assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
+        assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+        assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+        assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+        assertEquals(TEST_VIEWER_CONFIG_FILE_PATH, cmd.viewerConfigFilePathArg)
+        assertEquals(null, cmd.legacyViewerConfigFilePathArg)
+        assertEquals(TEST_LEGACY_OUTPUT_FILE_PATH, cmd.legacyOutputFilePath)
+        assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
+        assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
     }
 
-    @Test(expected = InvalidCommandException::class)
-    fun transformClasses_noProtoLogCacheClass() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
+    @Test
+    fun transformClasses_noLegacyOutputFile() {
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+        assertEquals(CommandOptions.TRANSFORM_CALLS_CMD, cmd.command)
+        assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+        assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+        assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+        assertEquals(TEST_VIEWER_CONFIG_FILE_PATH, cmd.viewerConfigFilePathArg)
+        assertEquals(TEST_LEGACY_VIEWER_CONFIG_FILE_PATH, cmd.legacyViewerConfigFilePathArg)
+        assertEquals(null, cmd.legacyOutputFilePath)
+        assertEquals(TEST_SRC_JAR, cmd.outputSourceJarArg)
+        assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
+    fun transformClasses_noProtoLogClass() {
+        val testLine = "transform-protolog-calls " +
+                "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+                "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+                "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("--protolog-class")
+    }
+
+    @Test
     fun transformClasses_noProtoLogGroupClass() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("--loggroups-class")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_noProtoLogGroupJar() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("--loggroups-jar")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_noOutJar() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
-                TEST_JAVA_SRC.joinToString(" ")
-        CommandOptions(testLine.split(' ').toTypedArray())
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+                "${TEST_JAVA_SRC.joinToString(" ")}"
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("--output-srcjar")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_noJavaInput() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("No java source input files")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_invalidProtoLogClass() {
-        val testLine = "transform-protolog-calls --protolog-class invalid " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
-                "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
-                "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
-                "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
-    }
-
-    @Test(expected = InvalidCommandException::class)
-    fun transformClasses_invalidProtoLogImplClass() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class invalid " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
-                "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
-                "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
-                "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
-    }
-
-    @Test(expected = InvalidCommandException::class)
-    fun transformClasses_invalidProtoLogCacheClass() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class invalid " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class invalid " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("class name invalid")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_invalidProtoLogGroupClass() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class invalid " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("class name invalid")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_invalidProtoLogGroupJar() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar invalid.txt " +
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat()
+                .contains("Jar file required, got invalid.txt instead")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_invalidOutJar() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
+        val testLine = "transform-protolog-calls " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
-                "--output-srcjar invalid.db ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+                "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+                "--output-srcjar invalid.pb ${TEST_JAVA_SRC.joinToString(" ")}"
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat()
+                .contains("Source jar file required, got invalid.pb instead")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_invalidJavaInput() {
-        val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
-                "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
-                "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
-                "--output-srcjar $TEST_SRC_JAR invalid.py"
-        CommandOptions(testLine.split(' ').toTypedArray())
+            val testLine = "transform-protolog-calls " +
+                    "--protolog-class $TEST_PROTOLOG_CLASS " +
+                    "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+                    "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                    "--viewer-config-file-path $TEST_VIEWER_CONFIG_FILE_PATH " +
+                    "--legacy-viewer-config-file-path $TEST_LEGACY_VIEWER_CONFIG_FILE_PATH " +
+                    "--legacy-output-file-path $TEST_LEGACY_OUTPUT_FILE_PATH " +
+                    "--output-srcjar $TEST_SRC_JAR invalid.py"
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat()
+                .contains("Not a java or kotlin source file invalid.py")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_unknownParam() {
         val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
                 "--unknown test --protolog-impl-class $TEST_PROTOLOGIMPL_CLASS " +
@@ -229,59 +312,88 @@ class CommandOptionsTest {
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("--unknown")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun transformClasses_noValue() {
         val testLine = "transform-protolog-calls --protolog-class $TEST_PROTOLOG_CLASS " +
-                "--protolog-impl-class " +
-                "--protolog-cache-class $TEST_PROTOLOGCACHE_CLASS " +
-                "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+                "--loggroups-class " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
                 "--output-srcjar $TEST_SRC_JAR ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("No value for --loggroups-class")
     }
 
     @Test
-    fun generateConfig() {
-        val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
+    fun generateConfig_json() {
+        val testLine = "generate-viewer-config " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
-                "--viewer-conf $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
+                "--viewer-config-type json " +
+                "--viewer-config $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
         val cmd = CommandOptions(testLine.split(' ').toTypedArray())
         assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command)
         assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
         assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
         assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
-        assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+        assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigFileNameArg)
         assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
+    fun generateConfig_proto() {
+        val testLine = "generate-viewer-config " +
+                "--protolog-class $TEST_PROTOLOG_CLASS " +
+                "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
+                "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
+                "--viewer-config-type proto " +
+                "--viewer-config $TEST_VIEWER_JSON ${TEST_JAVA_SRC.joinToString(" ")}"
+        val cmd = CommandOptions(testLine.split(' ').toTypedArray())
+        assertEquals(CommandOptions.GENERATE_CONFIG_CMD, cmd.command)
+        assertEquals(TEST_PROTOLOG_CLASS, cmd.protoLogClassNameArg)
+        assertEquals(TEST_PROTOLOGGROUP_CLASS, cmd.protoLogGroupsClassNameArg)
+        assertEquals(TEST_PROTOLOGGROUP_JAR, cmd.protoLogGroupsJarArg)
+        assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigFileNameArg)
+        assertEquals(TEST_JAVA_SRC, cmd.javaSourceArgs)
+    }
+
+    @Test
     fun generateConfig_noViewerConfig() {
         val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
                 TEST_JAVA_SRC.joinToString(" ")
-        CommandOptions(testLine.split(' ').toTypedArray())
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("--viewer-config required")
     }
 
-    @Test(expected = InvalidCommandException::class)
+    @Test
     fun generateConfig_invalidViewerConfig() {
         val testLine = "generate-viewer-config --protolog-class $TEST_PROTOLOG_CLASS " +
                 "--loggroups-class $TEST_PROTOLOGGROUP_CLASS " +
                 "--loggroups-jar $TEST_PROTOLOGGROUP_JAR " +
-                "--viewer-conf invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}"
-        CommandOptions(testLine.split(' ').toTypedArray())
+                "--viewer-config invalid.yaml ${TEST_JAVA_SRC.joinToString(" ")}"
+        val exception = assertThrows<InvalidCommandException>(InvalidCommandException::class.java) {
+            CommandOptions(testLine.split(' ').toTypedArray())
+        }
+        assertThat(exception).hasMessageThat().contains("required, got invalid.yaml instead")
     }
 
     @Test
     fun readLog() {
-        val testLine = "read-log --viewer-conf $TEST_VIEWER_JSON $TEST_LOG"
+        val testLine = "read-log --viewer-config $TEST_VIEWER_JSON $TEST_LOG"
         val cmd = CommandOptions(testLine.split(' ').toTypedArray())
         assertEquals(CommandOptions.READ_LOG_CMD, cmd.command)
-        assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigJsonArg)
+        assertEquals(TEST_VIEWER_JSON, cmd.viewerConfigFileNameArg)
         assertEquals(TEST_LOG, cmd.logProtofileArg)
     }
 }
diff --git a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
index 0d2b91d6cfb87ff3b8aeb590c9454d5548e8d44a..822118cc53432f67da8b6534d9c97edf1ca2be28 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt
@@ -16,22 +16,24 @@
 
 package com.android.protolog.tool
 
-import org.junit.Assert
-import org.junit.Assert.assertTrue
-import org.junit.Test
+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.Test
 
 class EndToEndTest {
 
     @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,26 +45,29 @@ class EndToEndTest {
                             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",
-                        "--protolog-impl-class", "com.android.internal.protolog.ProtoLogImpl",
-                        "--protolog-cache-class",
-                        "com.android.server.wm.ProtoLogCache",
                         "--loggroups-class", "com.android.internal.protolog.ProtoLogGroup",
                         "--loggroups-jar", "not_required.jar",
+                        "--viewer-config-file-path", "not_required.pb",
                         "--output-srcjar", "out.srcjar",
                         "frameworks/base/org/example/Example.java"))
         )
         val outSrcJar = assertLoadSrcJar(output, "out.srcjar")
-        assertTrue(" 2066303299," in outSrcJar["frameworks/base/org/example/Example.java"]!!)
+        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;
@@ -74,17 +79,27 @@ class EndToEndTest {
                             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",
                         "--loggroups-class", "com.android.internal.protolog.ProtoLogGroup",
                         "--loggroups-jar", "not_required.jar",
-                        "--viewer-conf", "out.json",
+                        "--viewer-config-type", "json",
+                        "--viewer-config", "out.json",
                         "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(
@@ -112,21 +127,46 @@ class EndToEndTest {
     }
 
     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/LogParserTest.kt b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
index 512d90c725fe8f30b02ae2168c16585d50064a57..1d3270268843d637fc10b5596c6dee26ef655278 100644
--- a/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
+++ b/tools/protologtool/tests/com/android/protolog/tool/LogParserTest.kt
@@ -35,7 +35,7 @@ import java.util.Locale
 class LogParserTest {
     private val configParser: ViewerConfigParser = mock(ViewerConfigParser::class.java)
     private val parser = LogParser(configParser)
-    private var config: MutableMap<Int, ViewerConfigParser.ConfigEntry> = mutableMapOf()
+    private var config: MutableMap<Long, ViewerConfigParser.ConfigEntry> = mutableMapOf()
     private var outStream: OutputStream = ByteArrayOutputStream()
     private var printStream: PrintStream = PrintStream(outStream)
     private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
similarity index 97%
rename from tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorTest.kt
rename to tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt
index 90b8059dae1cbb28f5eb6f65594cfe394c04b6da..5e50f71d75cca442db8014ae8fc97452f7c9db0d 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 com.github.javaparser.ast.expr.MethodCallExpr
 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 @@ class ProtoLogCallProcessorTest {
 
     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 ea9a58d859af989af095f69cc369b83b0dd2e3dc..0000000000000000000000000000000000000000
--- 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 f52bfeccea515d89899b06d7b5b4959a8beae6fb..de0b5bae118e64dcc440169c4032d90ecfe955c7 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.android.internal.protolog.common.LogLevel
 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 SourceTransformerTest {
 
             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 SourceTransformerTest {
 
             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 SourceTransformerTest {
 
             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 SourceTransformerTest {
 
             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 SourceTransformerTest {
 
             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 @@ class SourceTransformerTest {
     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 @@ class SourceTransformerTest {
         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 @@ class SourceTransformerTest {
     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 @@ class SourceTransformerTest {
         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 @@ class SourceTransformerTest {
         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 @@ class SourceTransformerTest {
     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 @@ class SourceTransformerTest {
         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 @@ class SourceTransformerTest {
     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 @@ class SourceTransformerTest {
         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 @@ class SourceTransformerTest {
     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 @@ class SourceTransformerTest {
         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 @@ class SourceTransformerTest {
         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 52dce21944f653b3375a073ae0ac444cad83e58c..d27ae88fc4889334336dc4c0ab3544834631f24d 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 @@ package com.android.protolog.tool
 
 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 @@ class ViewerConfigBuilderTest {
         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 @@ class ViewerConfigBuilderTest {
 
     @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)])
-    }
-
-    @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)])
+            LogLevel.INFO, GROUP1)])
     }
-
-    private fun List<LogCall>.withContext() = map { it to ParsingContext() }
 }