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

Commit e6d8f6f9 authored by Mykola Podolian's avatar Mykola Podolian Committed by Android (Google) Code Review
Browse files

Merge "Added initial bubble debug logging classes." into main

parents e9c8de53 6423b30e
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()
    }
}