Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6423b30e authored by mpodolian's avatar mpodolian
Browse files

Added initial bubble debug logging classes.

Bug: 436320481
Flag: com.android.wm.shell.enable_bubble_bar
Test: BubbleEventHistoryLoggerTest

Change-Id: I62ef1c3f2db4b4124e355ba775af69b45610cb74
parent 07d74c4c
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.bubbles.logging

/** Represents an event that happened to bubbles, for debugging purposes. */
data class BubbleEvent(

    /** What happened to the bubbles. */
    val title: String,

    /** Additional optional event data. */
    val eventData: String? = null,

    /** Event occurrence time in milliseconds since epoch */
    val timestamp: Long,
)
+88 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.bubbles.logging

import android.icu.text.SimpleDateFormat
import android.text.TextUtils
import androidx.annotation.VisibleForTesting
import java.io.PrintWriter
import java.util.Locale

/**
 * An implementation of [DebugLogger] that stores a history of events, respecting the [MAX_EVENTS]
 * limit. It can add events history as part of a system dump method.
 */
class BubbleEventHistoryLogger : DebugLogger {

    private val recentEvents: MutableList<BubbleEvent> = mutableListOf()

    override fun d(message: String, vararg parameters: Any, eventData: String?) {
        logEvent("d: ${TextUtils.formatSimple(message, *parameters)}", eventData)
    }

    override fun v(message: String, vararg parameters: Any, eventData: String?) {
        logEvent("v: ${TextUtils.formatSimple(message, *parameters)}", eventData)
    }

    override fun i(message: String, vararg parameters: Any, eventData: String?) {
        logEvent("i: ${TextUtils.formatSimple(message, *parameters)}", eventData)
    }

    override fun w(message: String, vararg parameters: Any, eventData: String?) {
        logEvent("w: ${TextUtils.formatSimple(message, *parameters)}", eventData)
    }

    override fun e(message: String, vararg parameters: Any, eventData: String?) {
        logEvent("e: ${TextUtils.formatSimple(message, *parameters)}", eventData)
    }

    @VisibleForTesting
    @Synchronized
    fun logEvent(
        title: String,
        eventData: String? = null,
        timestamp: Long = System.currentTimeMillis(),
    ) {
        if (recentEvents.size >= MAX_EVENTS) {
            recentEvents.removeAt(0)
        }
        recentEvents.add(BubbleEvent(title, eventData, timestamp))
    }

    /**
     * Dumps the collected events history to the provided [PrintWriter], adding the [prefix] at the
     * beginning of each line.
     */
    fun dump(prefix: String = "", pw: PrintWriter) {
        val recentEventsCopy = synchronized(this) { ArrayList(recentEvents) }
        pw.println("${prefix}Bubbles events history:")
        recentEventsCopy.reversed().forEach { event ->
            val eventFormattedTime = DATE_FORMATTER.format(event.timestamp)
            var eventData = ""
            if (!event.eventData.isNullOrBlank()) {
                eventData = " | ${event.eventData}"
            }
            pw.println("$prefix  $eventFormattedTime ${event.title} $eventData)")
        }
    }

    companion object {
        const val DATE_FORMAT = "MM-dd HH:mm:ss.SSS"
        const val MAX_EVENTS: Int = 25
        val DATE_FORMATTER = SimpleDateFormat(DATE_FORMAT, Locale.US)
    }
}
+73 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.bubbles.logging

/**
 * Bubbles debug logger interface.
 *
 * Usage of this API is similar to the `android.utils.Log` class.
 *
 * Instead of plain text log messages, each call consists of a `messageString` (which is a format
 * string for the log message and must be a string literal or a concatenation of string literals)
 * and a vararg array of parameters for the formatter.
 *
 * The syntax for the message string is based on [android.text.TextUtils.formatSimple].
 *
 * All methods are thread safe.
 */
interface DebugLogger {

    /**
     * Logs a DEBUG level message.
     *
     * The [message] is a format string, with [parameters] substituted into it. An optional
     * [eventData] string may also be provided, which implementations can include in the log output.
     */
    fun d(message: String, vararg parameters: Any, eventData: String? = null) {}

    /**
     * Logs a VERBOSE level message.
     *
     * The [message] is a format string, with [parameters] substituted into it. An optional
     * [eventData] string may also be provided, which implementations can include in the log output.
     */
    fun v(message: String, vararg parameters: Any, eventData: String? = null) {}

    /**
     * Logs an INFO level message.
     *
     * The [message] is a format string, with [parameters] substituted into it. An optional
     * [eventData] string may also be provided, which implementations can include in the log output.
     */
    fun i(message: String, vararg parameters: Any, eventData: String? = null) {}

    /**
     * Logs a WARNING level message.
     *
     * The [message] is a format string, with [parameters] substituted into it. An optional
     * [eventData] string may also be provided, which implementations can include in the log output.
     */
    fun w(message: String, vararg parameters: Any, eventData: String? = null) {}

    /**
     * Logs an ERROR level message.
     *
     * The [message] is a format string, with [parameters] substituted into it. An optional
     * [eventData] string may also be provided, which implementations can include in the log output.
     */
    fun e(message: String, vararg parameters: Any, eventData: String? = null) {}
}
+141 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.shared.bubbles.logging

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.wm.shell.shared.bubbles.logging.BubbleEventHistoryLogger.Companion.DATE_FORMATTER
import com.android.wm.shell.shared.bubbles.logging.BubbleEventHistoryLogger.Companion.MAX_EVENTS
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.text.split
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

/** Unit tests for [BubbleEventHistoryLogger]. */
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleEventHistoryLoggerTest {

    private val logger = BubbleEventHistoryLogger()
    private val logPattern = Regex("^\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3} .: .*$")

    @Test
    fun dump_printsHeaderWhenNoEvents() {
        val expectedOutput = "Bubbles events history:\n"
        assertThat(getDumpOutput()).isEqualTo(expectedOutput)
    }

    @Test
    fun dump_RespectsMAX_EVENTS() {
        repeat(MAX_EVENTS + 10) { logger.d(message = "title") }
        val linesCount = getTrimmedLogLines().size

        assertThat(linesCount).isEqualTo(MAX_EVENTS)
    }

    @Test
    fun dump_PrintsEventsInReverseChronologicalOrderStartingFromTheMostRecentEvent() {
        val repetitions = MAX_EVENTS * 2
        repeat(repetitions) { repetition ->
            logger.logEvent(title = "", timestamp = repetition.toLong())
        }
        val lastEventDateTime = DATE_FORMATTER.format(repetitions - 1)
        val logLines = getTrimmedLogLines()

        // reversed timestamps should be in chronological order
        assertThat(logLines.reversed()).isInOrder()
        // first log entry corresponds to the most recent event
        assertThat(logLines.first()).contains(lastEventDateTime)
    }

    @Test
    fun dump_printsEventsInExpectedFormat() {
        logger.d("test %b", true, eventData = "eventData")
        logger.v("test %d", 0, eventData = "eventData")
        logger.i("test %s", "stringArgument", eventData = "eventData")
        logger.w("test")
        logger.e("test", eventData = "eventData")

        val logLines = getTrimmedLogLines()

        assertThat(checkLogFormat(logLines[4], 'd', "test true", "eventData")).isTrue()
        assertThat(checkLogFormat(logLines[3], 'v', "test 0", "eventData")).isTrue()
        assertThat(checkLogFormat(logLines[2], 'i', "test stringArgument", "eventData")).isTrue()
        assertThat(checkLogFormat(logLines[1], 'w', "test")).isTrue()
        assertThat(checkLogFormat(logLines[0], 'e', "test", "eventData")).isTrue()
    }

    @Test
    fun multiThreadLogging_dump_RespectsMAX_EVENTS() {
        val numberOfThreads = 50
        val eventsPerThread = MAX_EVENTS // each thread will emmit MAX_EVENTS
        val startLatch = CountDownLatch(1) // Main thread signals worker threads to start
        val doneLatch = CountDownLatch(numberOfThreads) // Worker threads signal
        val executorService = Executors.newFixedThreadPool(numberOfThreads)
        for (i in 0 until numberOfThreads) {
            executorService.submit {
                try {
                    startLatch.await() // Wait until the main thread gives the green light
                    repeat(eventsPerThread) { logger.logEvent("Thread $i", "Data $i-$it") }
                } finally {
                    doneLatch.countDown() // Signal that this thread has finished
                }
            }
        }

        // Give all threads the signal to start logging which unblocks all threads
        startLatch.countDown()
        // Wait for all threads to complete their logging tasks
        // Add a timeout to prevent the test from hanging indefinitely if something goes wrong
        assertThat(doneLatch.await(5, TimeUnit.SECONDS)).isTrue()

        // Check that there are no more than MAX_EVENTS events in the log history
        val logLinesCount = getTrimmedLogLines().size
        assertThat(logLinesCount).isEqualTo(MAX_EVENTS)
    }

    private fun checkLogFormat(
        logEntry: String,
        logLevel: Char,
        title: String,
        eventData: String? = null,
    ): Boolean {
        val matchesEventData = eventData.isNullOrBlank() || logEntry.contains("| $eventData")
        return logEntry.matches(logPattern) &&
            logEntry.contains(logLevel) &&
            logEntry.contains(title) &&
            matchesEventData
    }

    private fun getTrimmedLogLines(): List<String> {
        return getDumpOutput().split("\n").drop(1).dropLast(1).map { it.trim() }
    }

    private fun getDumpOutput(): String {
        val stringWriter = StringWriter()
        val printWriter = PrintWriter(stringWriter)

        logger.dump(pw = printWriter)
        printWriter.flush() // Ensure all content is written to StringWriter
        return stringWriter.toString()
    }
}