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()
+ }
+}