Move LogBuffer to sysui/plugins

Consequently, RingBuffer is also moved to
SysUI/plugins

Test: builds, logs still work (see dumpsys)
Test: atest SystemUITests
Bug: 253490717
Change-Id: I91e3ef3ebdc6a2d929b2c1ae8ef158aeffa4c733
Merged-In: I91e3ef3ebdc6a2d929b2c1ae8ef158aeffa4c733
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index cafaaf8..7709f21 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -33,6 +33,7 @@
 
     static_libs: [
         "androidx.annotation_annotation",
+        "error_prone_annotations",
         "PluginCoreLib",
         "SystemUIAnimationLib",
     ],
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
new file mode 100644
index 0000000..6436dcb
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
@@ -0,0 +1,298 @@
+/*
+ * 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.systemui.plugins.log
+
+import android.os.Trace
+import android.util.Log
+import com.android.systemui.plugins.util.RingBuffer
+import com.google.errorprone.annotations.CompileTimeConstant
+import java.io.PrintWriter
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.BlockingQueue
+import kotlin.concurrent.thread
+import kotlin.math.max
+
+/**
+ * A simple ring buffer of recyclable log messages
+ *
+ * The goal of this class is to enable logging that is both extremely chatty and extremely
+ * lightweight. If done properly, logging a message will not result in any heap allocations or
+ * string generation. Messages are only converted to strings if the log is actually dumped (usually
+ * as the result of taking a bug report).
+ *
+ * You can dump the entire buffer at any time by running:
+ *
+ * ```
+ * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService <bufferName>
+ * ```
+ *
+ * ...where `bufferName` is the (case-sensitive) [name] passed to the constructor.
+ *
+ * By default, only messages of WARN level or higher are echoed to logcat, but this can be adjusted
+ * locally (usually for debugging purposes).
+ *
+ * To enable logcat echoing for an entire buffer:
+ *
+ * ```
+ * $ adb shell settings put global systemui/buffer/<bufferName> <level>
+ * ```
+ *
+ * To enable logcat echoing for a specific tag:
+ *
+ * ```
+ * $ adb shell settings put global systemui/tag/<tag> <level>
+ * ```
+ *
+ * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or
+ * the first letter of any of the previous.
+ *
+ * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI
+ * LogBufferFactory.
+ *
+ * @param name The name of this buffer, printed when the buffer is dumped and in some other
+ * situations.
+ * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
+ * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the
+ * maximum, it behaves like a ring buffer.
+ */
+class LogBuffer
+@JvmOverloads
+constructor(
+    private val name: String,
+    private val maxSize: Int,
+    private val logcatEchoTracker: LogcatEchoTracker,
+    private val systrace: Boolean = true,
+) {
+    private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
+
+    private val echoMessageQueue: BlockingQueue<LogMessage>? =
+        if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
+
+    init {
+        if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
+            thread(start = true, name = "LogBuffer-$name", priority = Thread.NORM_PRIORITY) {
+                try {
+                    while (true) {
+                        echoToDesiredEndpoints(echoMessageQueue.take())
+                    }
+                } catch (e: InterruptedException) {
+                    Thread.currentThread().interrupt()
+                }
+            }
+        }
+    }
+
+    var frozen = false
+        private set
+
+    private val mutable
+        get() = !frozen && maxSize > 0
+
+    /**
+     * Logs a message to the log buffer
+     *
+     * May also log the message to logcat if echoing is enabled for this buffer or tag.
+     *
+     * The actual string of the log message is not constructed until it is needed. To accomplish
+     * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is
+     * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data
+     * on the message's fields. The message is then inserted into the buffer where it waits until it
+     * is either pushed out by newer messages or it needs to printed. If and when this latter moment
+     * occurs, the [messagePrinter] function is called on the message. It reads whatever data the
+     * initializer stored and converts it to a human-readable log message.
+     *
+     * @param tag A string of at most 23 characters, used for grouping logs into categories or
+     * subjects. If this message is echoed to logcat, this will be the tag that is used.
+     * @param level Which level to log the message at, both to the buffer and to logcat if it's
+     * echoed. In general, a module should split most of its logs into either INFO or DEBUG level.
+     * INFO level should be reserved for information that other parts of the system might care
+     * about, leaving the specifics of code's day-to-day operations to DEBUG.
+     * @param messageInitializer A function that will be called immediately to store relevant data
+     * on the log message. The value of `this` will be the LogMessage to be initialized.
+     * @param messagePrinter A function that will be called if and when the message needs to be
+     * dumped to logcat or a bug report. It should read the data stored by the initializer and
+     * convert it to a human-readable string. The value of `this` will be the LogMessage to be
+     * printed. **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and
+     * NEVER any variables in its enclosing scope. Otherwise, the runtime will need to allocate a
+     * new instance of the printer for each call, thwarting our attempts at avoiding any sort of
+     * allocation.
+     * @param exception Provide any exception that need to be logged. This is saved as
+     * [LogMessage.exception]
+     */
+    @JvmOverloads
+    inline fun log(
+        tag: String,
+        level: LogLevel,
+        messageInitializer: MessageInitializer,
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+    ) {
+        val message = obtain(tag, level, messagePrinter, exception)
+        messageInitializer(message)
+        commit(message)
+    }
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer. Use sparingly.
+     *
+     * May also log the message to logcat if echoing is enabled for this buffer or tag. This is for
+     * simpler use-cases where [message] is a compile time string constant. For use-cases where the
+     * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in
+     * an initializer and a message printer.
+     *
+     * Log buffers are limited by the number of entries, so logging more frequently will limit the
+     * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a
+     * bug report more actionable, so using the [log] with a messagePrinter to add more detail to
+     * every log may do more to improve overall logging than adding more logs with this method.
+     */
+    fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
+        log(tag, level, { str1 = message }, { str1!! })
+
+    /**
+     * You should call [log] instead of this method.
+     *
+     * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size,
+     * grows the buffer by one.
+     *
+     * After calling [obtain], the message will now be at the end of the buffer. The caller must
+     * store any relevant data on the message and then call [commit].
+     */
+    @Synchronized
+    fun obtain(
+        tag: String,
+        level: LogLevel,
+        messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+    ): LogMessage {
+        if (!mutable) {
+            return FROZEN_MESSAGE
+        }
+        val message = buffer.advance()
+        message.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception)
+        return message
+    }
+
+    /**
+     * You should call [log] instead of this method.
+     *
+     * After acquiring a message via [obtain], call this method to signal to the buffer that you
+     * have finished filling in its data fields. The message will be echoed to logcat if necessary.
+     */
+    @Synchronized
+    fun commit(message: LogMessage) {
+        if (!mutable) {
+            return
+        }
+        // Log in the background thread only if echoMessageQueue exists and has capacity (checking
+        // capacity avoids the possibility of blocking this thread)
+        if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) {
+            try {
+                echoMessageQueue.put(message)
+            } catch (e: InterruptedException) {
+                // the background thread has been shut down, so just log on this one
+                echoToDesiredEndpoints(message)
+            }
+        } else {
+            echoToDesiredEndpoints(message)
+        }
+    }
+
+    /** Sends message to echo after determining whether to use Logcat and/or systrace. */
+    private fun echoToDesiredEndpoints(message: LogMessage) {
+        val includeInLogcat =
+            logcatEchoTracker.isBufferLoggable(name, message.level) ||
+                logcatEchoTracker.isTagLoggable(message.tag, message.level)
+        echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
+    }
+
+    /** Converts the entire buffer to a newline-delimited string */
+    @Synchronized
+    fun dump(pw: PrintWriter, tailLength: Int) {
+        val iterationStart =
+            if (tailLength <= 0) {
+                0
+            } else {
+                max(0, buffer.size - tailLength)
+            }
+
+        for (i in iterationStart until buffer.size) {
+            buffer[i].dump(pw)
+        }
+    }
+
+    /**
+     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls
+     * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if
+     * necessary.
+     */
+    @Synchronized
+    fun freeze() {
+        if (!frozen) {
+            log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 frozen" })
+            frozen = true
+        }
+    }
+
+    /** Undoes the effects of calling [freeze]. */
+    @Synchronized
+    fun unfreeze() {
+        if (frozen) {
+            log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 unfrozen" })
+            frozen = false
+        }
+    }
+
+    private fun echo(message: LogMessage, toLogcat: Boolean, toSystrace: Boolean) {
+        if (toLogcat || toSystrace) {
+            val strMessage = message.messagePrinter(message)
+            if (toSystrace) {
+                echoToSystrace(message, strMessage)
+            }
+            if (toLogcat) {
+                echoToLogcat(message, strMessage)
+            }
+        }
+    }
+
+    private fun echoToSystrace(message: LogMessage, strMessage: String) {
+        Trace.instantForTrack(
+            Trace.TRACE_TAG_APP,
+            "UI Events",
+            "$name - ${message.level.shortString} ${message.tag}: $strMessage"
+        )
+    }
+
+    private fun echoToLogcat(message: LogMessage, strMessage: String) {
+        when (message.level) {
+            LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception)
+            LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception)
+            LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception)
+            LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception)
+            LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception)
+            LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception)
+        }
+    }
+}
+
+/**
+ * A function that will be called immediately to store relevant data on the log message. The value
+ * of `this` will be the LogMessage to be initialized.
+ */
+typealias MessageInitializer = LogMessage.() -> Unit
+
+private const val TAG = "LogBuffer"
+private val FROZEN_MESSAGE = LogMessageImpl.create()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
new file mode 100644
index 0000000..b036cf0
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.systemui.plugins.log
+
+import android.util.Log
+
+/** Enum version of @Log.Level */
+enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) {
+    VERBOSE(Log.VERBOSE, "V"),
+    DEBUG(Log.DEBUG, "D"),
+    INFO(Log.INFO, "I"),
+    WARNING(Log.WARN, "W"),
+    ERROR(Log.ERROR, "E"),
+    WTF(Log.ASSERT, "WTF")
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
new file mode 100644
index 0000000..9468681
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.systemui.plugins.log
+
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+/**
+ * Generic data class for storing messages logged to a [LogBuffer]
+ *
+ * Each LogMessage has a few standard fields ([level], [tag], and [timestamp]). The rest are generic
+ * data slots that may or may not be used, depending on the nature of the specific message being
+ * logged.
+ *
+ * When a message is logged, the code doing the logging stores data in one or more of the generic
+ * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
+ * [messagePrinter] function reads the data stored in the generic fields and converts that to a
+ * human- readable string. Thus, for every log type there must be a specialized initializer function
+ * that stores data specific to that log type and a specialized printer function that prints that
+ * data.
+ *
+ * See [LogBuffer.log] for more information.
+ */
+interface LogMessage {
+    val level: LogLevel
+    val tag: String
+    val timestamp: Long
+    val messagePrinter: MessagePrinter
+    val exception: Throwable?
+
+    var str1: String?
+    var str2: String?
+    var str3: String?
+    var int1: Int
+    var int2: Int
+    var long1: Long
+    var long2: Long
+    var double1: Double
+    var bool1: Boolean
+    var bool2: Boolean
+    var bool3: Boolean
+    var bool4: Boolean
+
+    /** Function that dumps the [LogMessage] to the provided [writer]. */
+    fun dump(writer: PrintWriter) {
+        val formattedTimestamp = DATE_FORMAT.format(timestamp)
+        val shortLevel = level.shortString
+        val messageToPrint = messagePrinter(this)
+        printLikeLogcat(writer, formattedTimestamp, shortLevel, tag, messageToPrint)
+        exception?.printStackTrace(writer)
+    }
+}
+
+/**
+ * A function that will be called if and when the message needs to be dumped to logcat or a bug
+ * report. It should read the data stored by the initializer and convert it to a human-readable
+ * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer
+ * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing
+ * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call,
+ * thwarting our attempts at avoiding any sort of allocation.
+ */
+typealias MessagePrinter = LogMessage.() -> String
+
+private fun printLikeLogcat(
+    pw: PrintWriter,
+    formattedTimestamp: String,
+    shortLogLevel: String,
+    tag: String,
+    message: String
+) {
+    pw.print(formattedTimestamp)
+    pw.print(" ")
+    pw.print(shortLogLevel)
+    pw.print(" ")
+    pw.print(tag)
+    pw.print(": ")
+    pw.println(message)
+}
+
+private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
new file mode 100644
index 0000000..f2a6a91
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.systemui.plugins.log
+
+/** Recyclable implementation of [LogMessage]. */
+data class LogMessageImpl(
+    override var level: LogLevel,
+    override var tag: String,
+    override var timestamp: Long,
+    override var messagePrinter: MessagePrinter,
+    override var exception: Throwable?,
+    override var str1: String?,
+    override var str2: String?,
+    override var str3: String?,
+    override var int1: Int,
+    override var int2: Int,
+    override var long1: Long,
+    override var long2: Long,
+    override var double1: Double,
+    override var bool1: Boolean,
+    override var bool2: Boolean,
+    override var bool3: Boolean,
+    override var bool4: Boolean,
+) : LogMessage {
+
+    fun reset(
+        tag: String,
+        level: LogLevel,
+        timestamp: Long,
+        renderer: MessagePrinter,
+        exception: Throwable? = null,
+    ) {
+        this.level = level
+        this.tag = tag
+        this.timestamp = timestamp
+        this.messagePrinter = renderer
+        this.exception = exception
+        str1 = null
+        str2 = null
+        str3 = null
+        int1 = 0
+        int2 = 0
+        long1 = 0
+        long2 = 0
+        double1 = 0.0
+        bool1 = false
+        bool2 = false
+        bool3 = false
+        bool4 = false
+    }
+
+    companion object Factory {
+        fun create(): LogMessageImpl {
+            return LogMessageImpl(
+                LogLevel.DEBUG,
+                DEFAULT_TAG,
+                0,
+                DEFAULT_PRINTER,
+                null,
+                null,
+                null,
+                null,
+                0,
+                0,
+                0,
+                0,
+                0.0,
+                false,
+                false,
+                false,
+                false
+            )
+        }
+    }
+}
+
+private const val DEFAULT_TAG = "UnknownTag"
+private val DEFAULT_PRINTER: MessagePrinter = { "Unknown message: $this" }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
new file mode 100644
index 0000000..cfe894f
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.systemui.plugins.log
+
+/** Keeps track of which [LogBuffer] messages should also appear in logcat. */
+interface LogcatEchoTracker {
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
+    fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean
+
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
+    fun isTagLoggable(tagName: String, level: LogLevel): Boolean
+
+    /** Whether to log messages in a background thread. */
+    val logInBackgroundThread: Boolean
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
new file mode 100644
index 0000000..d3fabac
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.systemui.plugins.log
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+
+/**
+ * Version of [LogcatEchoTracker] for debuggable builds
+ *
+ * The log level of individual buffers or tags can be controlled via global settings:
+ *
+ * ```
+ * # Echo any message to <bufferName> of <level> or higher
+ * $ adb shell settings put global systemui/buffer/<bufferName> <level>
+ *
+ * # Echo any message of <tag> and of <level> or higher
+ * $ adb shell settings put global systemui/tag/<tag> <level>
+ * ```
+ */
+class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) :
+    LogcatEchoTracker {
+    private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf()
+    private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf()
+    override val logInBackgroundThread = true
+
+    companion object Factory {
+        @JvmStatic
+        fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug {
+            val tracker = LogcatEchoTrackerDebug(contentResolver)
+            tracker.attach(mainLooper)
+            return tracker
+        }
+    }
+
+    private fun attach(mainLooper: Looper) {
+        contentResolver.registerContentObserver(
+            Settings.Global.getUriFor(BUFFER_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedBufferLevels.clear()
+                }
+            }
+        )
+
+        contentResolver.registerContentObserver(
+            Settings.Global.getUriFor(TAG_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedTagLevels.clear()
+                }
+            }
+        )
+    }
+
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
+    @Synchronized
+    override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
+        return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal
+    }
+
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
+    @Synchronized
+    override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
+        return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels)
+    }
+
+    private fun getLogLevel(
+        name: String,
+        path: String,
+        cache: MutableMap<String, LogLevel>
+    ): LogLevel {
+        return cache[name] ?: readSetting("$path/$name").also { cache[name] = it }
+    }
+
+    private fun readSetting(path: String): LogLevel {
+        return try {
+            parseProp(Settings.Global.getString(contentResolver, path))
+        } catch (_: Settings.SettingNotFoundException) {
+            DEFAULT_LEVEL
+        }
+    }
+
+    private fun parseProp(propValue: String?): LogLevel {
+        return when (propValue?.lowercase()) {
+            "verbose" -> LogLevel.VERBOSE
+            "v" -> LogLevel.VERBOSE
+            "debug" -> LogLevel.DEBUG
+            "d" -> LogLevel.DEBUG
+            "info" -> LogLevel.INFO
+            "i" -> LogLevel.INFO
+            "warning" -> LogLevel.WARNING
+            "warn" -> LogLevel.WARNING
+            "w" -> LogLevel.WARNING
+            "error" -> LogLevel.ERROR
+            "e" -> LogLevel.ERROR
+            "assert" -> LogLevel.WTF
+            "wtf" -> LogLevel.WTF
+            else -> DEFAULT_LEVEL
+        }
+    }
+}
+
+private val DEFAULT_LEVEL = LogLevel.WARNING
+private const val BUFFER_PATH = "systemui/buffer"
+private const val TAG_PATH = "systemui/tag"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
new file mode 100644
index 0000000..3c8bda4
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.systemui.plugins.log
+
+/** Production version of [LogcatEchoTracker] that isn't configurable. */
+class LogcatEchoTrackerProd : LogcatEchoTracker {
+    override val logInBackgroundThread = false
+
+    override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
+        return level >= LogLevel.WARNING
+    }
+
+    override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
+        return level >= LogLevel.WARNING
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
new file mode 100644
index 0000000..68d7890
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 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.systemui.plugins.util
+
+import kotlin.math.max
+
+/**
+ * A simple ring buffer implementation
+ *
+ * Use [advance] to get the least recent item in the buffer (and then presumably fill it with
+ * appropriate data). This will cause it to become the most recent item.
+ *
+ * As the buffer is used, it will grow, allocating new instances of T using [factory] until it
+ * reaches [maxSize]. After this point, no new instances will be created. Instead, the "oldest"
+ * instances will be recycled from the back of the buffer and placed at the front.
+ *
+ * @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring.
+ * @param factory A function that creates a fresh instance of T. Used by the buffer while it's
+ * growing to [maxSize].
+ */
+class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> {
+
+    private val buffer = MutableList<T?>(maxSize) { null }
+
+    /**
+     * An abstract representation that points to the "end" of the buffer. Increments every time
+     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the
+     * backing array. Always points to the "next" available slot in the buffer. Before the buffer
+     * has completely filled, the value pointed to will be null. Afterward, it will be the value at
+     * the "beginning" of the buffer.
+     *
+     * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
+     * omega will overflow after a little under three million years of continuous operation.
+     */
+    private var omega: Long = 0
+
+    /**
+     * The number of items currently stored in the buffer. Calls to [advance] will cause this value
+     * to increase by one until it reaches [maxSize].
+     */
+    val size: Int
+        get() = if (omega < maxSize) omega.toInt() else maxSize
+
+    /**
+     * Advances the buffer's position by one and returns the value that is now present at the "end"
+     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise,
+     * reuses the value that was previously at the "beginning" of the buffer.
+     *
+     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was
+     * previously stored on it.
+     */
+    fun advance(): T {
+        val index = indexOf(omega)
+        omega += 1
+        val entry = buffer[index] ?: factory().also { buffer[index] = it }
+        return entry
+    }
+
+    /**
+     * Returns the value stored at [index], which can range from 0 (the "start", or oldest element
+     * of the buffer) to [size]
+     * - 1 (the "end", or newest element of the buffer).
+     */
+    operator fun get(index: Int): T {
+        if (index < 0 || index >= size) {
+            throw IndexOutOfBoundsException("Index $index is out of bounds")
+        }
+
+        // If omega is larger than the maxSize, then the buffer is full, and omega is equivalent
+        // to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is
+        // not yet full and our start should be 0. However, in modspace, maxSize and 0 are
+        // equivalent, so we can get away with using it as the start value instead.
+        val start = max(omega, maxSize.toLong())
+
+        return buffer[indexOf(start + index)]!!
+    }
+
+    inline fun forEach(action: (T) -> Unit) {
+        for (i in 0 until size) {
+            action(get(i))
+        }
+    }
+
+    override fun iterator(): Iterator<T> {
+        return object : Iterator<T> {
+            private var position: Int = 0
+
+            override fun next(): T {
+                if (position >= size) {
+                    throw NoSuchElementException()
+                }
+                return get(position).also { position += 1 }
+            }
+
+            override fun hasNext(): Boolean {
+                return position < size
+            }
+        }
+    }
+
+    private fun indexOf(position: Long): Int {
+        return (position % maxSize).toInt()
+    }
+}
diff --git a/packages/SystemUI/plugin/tests/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
new file mode 100644
index 0000000..a39b856
--- /dev/null
+++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
@@ -0,0 +1,138 @@
+package com.android.systemui.log
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.log.LogBuffer
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+
+@SmallTest
+@RunWith(MockitoJUnitRunner::class)
+class LogBufferTest : SysuiTestCase() {
+    private lateinit var buffer: LogBuffer
+
+    private lateinit var outputWriter: StringWriter
+
+    @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker
+
+    @Before
+    fun setup() {
+        outputWriter = StringWriter()
+        buffer = createBuffer()
+    }
+
+    private fun createBuffer(): LogBuffer {
+        return LogBuffer("TestBuffer", 1, logcatEchoTracker, false)
+    }
+
+    @Test
+    fun log_shouldSaveLogToBuffer() {
+        buffer.log("Test", LogLevel.INFO, "Some test message")
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("Some test message")
+    }
+
+    @Test
+    fun log_shouldRotateIfLogBufferIsFull() {
+        buffer.log("Test", LogLevel.INFO, "This should be rotated")
+        buffer.log("Test", LogLevel.INFO, "New test message")
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("New test message")
+    }
+
+    @Test
+    fun dump_writesExceptionAndStacktrace() {
+        buffer = createBuffer()
+        val exception = createTestException("Exception message", "TestClass")
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("Extra message")
+        assertThat(dumpedString).contains("java.lang.RuntimeException: Exception message")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
+    }
+
+    @Test
+    fun dump_writesCauseAndStacktrace() {
+        buffer = createBuffer()
+        val exception =
+            createTestException(
+                "Exception message",
+                "TestClass",
+                cause = createTestException("The real cause!", "TestClass")
+            )
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
+    }
+
+    @Test
+    fun dump_writesSuppressedExceptionAndStacktrace() {
+        buffer = createBuffer()
+        val exception = RuntimeException("Root exception message")
+        exception.addSuppressed(
+            createTestException(
+                "First suppressed exception",
+                "FirstClass",
+                createTestException("Cause of suppressed exp", "ThirdClass")
+            )
+        )
+        exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass"))
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedStr = dumpBuffer()
+
+        // first suppressed exception
+        assertThat(dumpedStr)
+            .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception")
+        assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)")
+        assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)")
+
+        assertThat(dumpedStr)
+            .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
+        assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)")
+        assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)")
+
+        // second suppressed exception
+        assertThat(dumpedStr)
+            .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception")
+        assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)")
+        assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)")
+    }
+
+    private fun createTestException(
+        message: String,
+        errorClass: String,
+        cause: Throwable? = null,
+    ): Exception {
+        val exception = RuntimeException(message, cause)
+        exception.stackTrace =
+            (1..5)
+                .map { lineNumber ->
+                    StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber)
+                }
+                .toTypedArray()
+        return exception
+    }
+
+    private fun dumpBuffer(): String {
+        buffer.dump(PrintWriter(outputWriter), tailLength = 100)
+        return outputWriter.toString()
+    }
+}