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

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

Merge "Added BubbleLog object." into main

parents 86b91736 3a25caf6
Loading
Loading
Loading
Loading
+2 −1
Original line number Original line Diff line number Diff line
@@ -28,7 +28,8 @@ import java.util.Locale
 */
 */
class BubbleEventHistoryLogger : DebugLogger {
class BubbleEventHistoryLogger : DebugLogger {


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


    override fun d(message: String, vararg parameters: Any, eventData: String?) {
    override fun d(message: String, vararg parameters: Any, eventData: String?) {
        logEvent("d: ${TextUtils.formatSimple(message, *parameters)}", eventData)
        logEvent("d: ${TextUtils.formatSimple(message, *parameters)}", eventData)
+101 −0
Original line number Original line 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.util.Log
import androidx.annotation.VisibleForTesting
import java.io.PrintWriter

/**
 * Logs debug events related to bubbles.
 *
 * By default, this logger uses [BubbleEventHistoryLogger] for event storage. Additional logging
 * behaviors can be introduced by adding more loggers via the [addLogger] method.
 *
 * The [dump] method outputs the history collected by the default [BubbleEventHistoryLogger].
 *
 * For logging parameters and methods please refer to [DebugLogger] class.
 */
object BubbleLog {

    const val TAG = "BubbleLog"

    private val bubbleEventHistoryLogger = BubbleEventHistoryLogger()
    @VisibleForTesting val loggers = mutableListOf<DebugLogger>(bubbleEventHistoryLogger)

    /**
     * Adds a new [DebugLogger] to the list of loggers used by this class.
     *
     * When events are logged (e.g., via the [d] method), they will be dispatched to all registered
     * loggers, including any added through this method.
     *
     * @param debugLogger The [DebugLogger] instance to add.
     */
    @JvmStatic
    fun addLogger(debugLogger: DebugLogger) {
        loggers.add(debugLogger)
    }

    /** Logs a DEBUG level message for all registered [DebugLogger]s. */
    @JvmOverloads
    @JvmStatic
    fun d(message: String, vararg parameters: Any = emptyArray(), eventData: String? = null) {
        logSafelyForAllLoggers { logger -> logger.d(message, *parameters, eventData = eventData) }
    }

    /** Logs a VERBOSE level message for all registered [DebugLogger]s. */
    @JvmOverloads
    @JvmStatic
    fun v(message: String, vararg parameters: Any = emptyArray(), eventData: String? = null) {
        logSafelyForAllLoggers { logger -> logger.v(message, *parameters, eventData = eventData) }
    }

    /** Logs an INFO level message for all registered [DebugLogger]s. */
    @JvmOverloads
    @JvmStatic
    fun i(message: String, vararg parameters: Any = emptyArray(), eventData: String? = null) {
        logSafelyForAllLoggers { logger -> logger.i(message, *parameters, eventData = eventData) }
    }

    /** Logs a WARNING level message for all registered [DebugLogger]s. */
    @JvmOverloads
    @JvmStatic
    fun w(message: String, vararg parameters: Any = emptyArray(), eventData: String? = null) {
        logSafelyForAllLoggers { logger -> logger.w(message, *parameters, eventData = eventData) }
    }

    /** Logs an ERROR level message for all registered [DebugLogger]s. */
    @JvmOverloads
    @JvmStatic
    fun e(message: String, vararg parameters: Any = emptyArray(), eventData: String? = null) {
        logSafelyForAllLoggers { logger -> logger.e(message, *parameters, eventData = eventData) }
    }

    private inline fun logSafelyForAllLoggers(logFunction: (DebugLogger) -> Unit) {
        for (logger in loggers) {
            try {
                logFunction.invoke(logger)
            } catch (e: Exception) {
                Log.e(TAG, "Exception while logging for $logger", e)
            }
        }
    }

    fun dump(prefix: String = "", pw: PrintWriter) {
        bubbleEventHistoryLogger.dump(prefix, pw)
    }
}
+161 −0
Original line number Original line 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.text.TextUtils
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
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

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

    @Before
    fun setup() {
        // Clear all loggers except BubbleEventHistoryLogger
        BubbleLog.loggers.removeIf { it !is BubbleEventHistoryLogger }
        val historyLogger = BubbleLog.loggers.first() as BubbleEventHistoryLogger
        // Clear BubbleEventHistoryLogger previous events
        historyLogger.recentEvents.clear()
    }

    @Test
    fun addLogger_addedLoggerHasAllEvents() {
        assertThat(BubbleLog.loggers.size).isEqualTo(1)
        val testLogger = TestLogger()
        val testData = "test data"
        BubbleLog.addLogger(testLogger)

        BubbleLog.d("debug test message", eventData = testData)
        BubbleLog.v("verbose test message", eventData = testData)
        BubbleLog.i("info test message", eventData = testData)
        BubbleLog.w("warning test message", eventData = testData)
        BubbleLog.e("error test message", eventData = testData)

        val testLogs = testLogger.logs
        assertThat(testLogs).containsExactly(
            "d: debug test message | $testData",
            "v: verbose test message | $testData",
            "i: info test message | $testData",
            "w: warning test message | $testData",
            "e: error test message | $testData"
        ).inOrder()
    }

    @Test
    fun exceptionsDoNotAffectBubbleLogger() {
        val errorLogger = ExceptionThrowingLogger()
        assertThat(BubbleLog.loggers.size).isEqualTo(1)
        BubbleLog.addLogger(errorLogger)

        BubbleLog.d("debug test message")

        assertThat(errorLogger.exceptionThrown).isTrue()
        assertThat(getTrimmedLogLines()).isNotEmpty()
    }

    @Test
    fun dump_logsHistoryEventLoggerOutput() {
        BubbleLog.d("debug test message boolean = %b", false)
        BubbleLog.v("verbose test message int = %d", 1)

        val loggerOutput = getDumpOutput { BubbleLog.dump(pw = it) }
        assertThat(loggerOutput).contains("Bubbles events history:")
        assertThat(getTrimmedLogLines().size).isEqualTo(2)
    }

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

    private fun getDumpOutput(dumpFunction: (PrintWriter) -> Unit): String {
        val stringWriter = StringWriter()
        val printWriter = PrintWriter(stringWriter)
        dumpFunction.invoke(printWriter)
        printWriter.flush() // Ensure all content is written to StringWriter
        return stringWriter.toString()
    }

    class TestLogger : DebugLogger {

        val logs = mutableListOf<String>()

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

        private fun logEvent(title: String, eventData: String? = null) {
            logs.add("$title | $eventData")
        }
    }

    class ExceptionThrowingLogger : DebugLogger {

        var exceptionThrown = false

        override fun d(message: String, vararg parameters: Any, eventData: String?) {
            throwException()
        }

        override fun v(message: String, vararg parameters: Any, eventData: String?) {
            throwException()
        }

        override fun i(message: String, vararg parameters: Any, eventData: String?) {
            throwException()
        }

        override fun w(message: String, vararg parameters: Any, eventData: String?) {
            throwException()
        }

        override fun e(message: String, vararg parameters: Any, eventData: String?) {
            throwException()
        }

        private fun throwException() {
            exceptionThrown = true
            throw RuntimeException()
        }
    }
}