diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 0bf345d2772..07a93ef2117 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased - +* [changed] `FirebaseDataConnect.logLevel` type changed from `LogLevel` to + `MutableStateFlow`. This enables apps to "collect" the flow to, + for example, update a UI component when the log level changes. + ([#6586](https://github.com/firebase/firebase-android-sdk/pull/6586)) # 16.0.0-beta03 * [changed] Requires Data Connect emulator version 1.6.1 or later for code generation. diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt index f24ccc3c298..96e5c1e4601 100644 --- a/firebase-dataconnect/api.txt +++ b/firebase-dataconnect/api.txt @@ -112,8 +112,7 @@ package com.google.firebase.dataconnect { public final class FirebaseDataConnectKt { method @NonNull public static com.google.firebase.dataconnect.FirebaseDataConnect getInstance(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Companion, @NonNull com.google.firebase.FirebaseApp app, @NonNull com.google.firebase.dataconnect.ConnectorConfig config, @NonNull com.google.firebase.dataconnect.DataConnectSettings settings = com.google.firebase.dataconnect.DataConnectSettings()); method @NonNull public static com.google.firebase.dataconnect.FirebaseDataConnect getInstance(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Companion, @NonNull com.google.firebase.dataconnect.ConnectorConfig config, @NonNull com.google.firebase.dataconnect.DataConnectSettings settings = com.google.firebase.dataconnect.DataConnectSettings()); - method @NonNull public static com.google.firebase.dataconnect.LogLevel getLogLevel(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Companion); - method public static void setLogLevel(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Companion, @NonNull com.google.firebase.dataconnect.LogLevel); + method @NonNull public static kotlinx.coroutines.flow.MutableStateFlow getLogLevel(@NonNull com.google.firebase.dataconnect.FirebaseDataConnect.Companion); } @kotlinx.serialization.Serializable(with=LocalDateSerializer::class) public final class LocalDate { diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt index 04781727bed..6e344d1efa6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/FirebaseDataConnect.kt @@ -23,6 +23,7 @@ import com.google.firebase.app import com.google.firebase.dataconnect.core.FirebaseDataConnectFactory import com.google.firebase.dataconnect.core.LoggerGlobals import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.modules.SerializersModule @@ -381,8 +382,14 @@ public fun FirebaseDataConnect.Companion.getInstance( /** * The log level used by all [FirebaseDataConnect] instances. * + * As a [MutableStateFlow], the log level can be changed by assigning [MutableStateFlow.value]. + * Also, the flow can be "collected" as a means of observing the log level, which may be useful in + * the case that a user interface shows a UI element, such as a checkbox, to represent whether debug + * logging is enabled. + * * The default log level is [LogLevel.WARN]. Setting this to [LogLevel.DEBUG] will enable debug * logging, which is especially useful when reporting issues to Google or investigating problems * yourself. Setting it to [LogLevel.NONE] will disable all logging. */ -public var FirebaseDataConnect.Companion.logLevel: LogLevel by LoggerGlobals::logLevel +public val FirebaseDataConnect.Companion.logLevel: MutableStateFlow + get() = LoggerGlobals.logLevel diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt index 7aeaa1c67eb..00514af944e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt @@ -30,5 +30,26 @@ public enum class LogLevel { WARN, /** Do not log anything. */ - NONE, + NONE; + + internal companion object { + + /** + * Returns one of the two given log levels, the one that is "noisier" (i.e. that logs more). + * + * It can be useful to figure out which of two log levels are noisier on log level change, to + * emit a message about the log level change at the noisiest level. + */ + fun noisiestOf(logLevel1: LogLevel, logLevel2: LogLevel): LogLevel = + when (logLevel1) { + DEBUG -> DEBUG + NONE -> logLevel2 + WARN -> + when (logLevel2) { + DEBUG -> DEBUG + WARN -> WARN + NONE -> WARN + } + } + } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt index d68cb8492c1..b079bf7c792 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt @@ -20,8 +20,17 @@ import android.util.Log import com.google.firebase.dataconnect.BuildConfig import com.google.firebase.dataconnect.LogLevel import com.google.firebase.dataconnect.core.LoggerGlobals.LOG_TAG +import com.google.firebase.dataconnect.core.LoggerGlobals.Logger import com.google.firebase.util.nextAlphanumericString import kotlin.random.Random +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach internal interface Logger { val name: String @@ -58,22 +67,27 @@ private class LoggerImpl(override val name: String) : Logger { internal object LoggerGlobals { const val LOG_TAG = "FirebaseDataConnect" - @Volatile var logLevel: LogLevel = LogLevel.WARN + val logLevel = + MutableStateFlow(LogLevel.WARN).also { logLevelFlow -> + val logger = Logger("LogLevelChange") + @OptIn(DelicateCoroutinesApi::class) + logger.logChanges(logLevelFlow.value, logLevelFlow, GlobalScope) + } inline fun Logger.debug(message: () -> Any?) { - if (logLevel <= LogLevel.DEBUG) debug("${message()}") + if (logLevel.value <= LogLevel.DEBUG) debug("${message()}") } fun Logger.debug(message: String) { - if (logLevel <= LogLevel.DEBUG) log(null, LogLevel.DEBUG, message) + if (logLevel.value <= LogLevel.DEBUG) log(null, LogLevel.DEBUG, message) } inline fun Logger.warn(message: () -> Any?) { - if (logLevel <= LogLevel.WARN) warn("${message()}") + if (logLevel.value <= LogLevel.WARN) warn("${message()}") } inline fun Logger.warn(exception: Throwable?, message: () -> Any?) { - if (logLevel <= LogLevel.WARN) warn(exception, "${message()}") + if (logLevel.value <= LogLevel.WARN) warn(exception, "${message()}") } fun Logger.warn(message: String) { @@ -81,8 +95,30 @@ internal object LoggerGlobals { } fun Logger.warn(exception: Throwable?, message: String) { - if (logLevel <= LogLevel.WARN) log(exception, LogLevel.WARN, message) + if (logLevel.value <= LogLevel.WARN) log(exception, LogLevel.WARN, message) } fun Logger(name: String): Logger = LoggerImpl(name) + + // Log a message each time the log level changes. This is intended to provide context when debug + // logging is enabled and no logs are produced, to at least confirm that debug logging has been + // enabled. Also, it will leave a "mark" in the logs when debug logging is _disabled_ to explain + // why the debug logs stop. + private fun Logger.logChanges( + initialLogLevel: LogLevel, + flow: Flow, + coroutineScope: CoroutineScope + ) { + val state = MutableStateFlow(initialLogLevel) + log(null, initialLogLevel, "Log level set to $initialLogLevel") + flow + .onEach { newLogLevel: LogLevel -> + val oldLogLevel = state.getAndUpdate { newLogLevel } + if (newLogLevel != oldLogLevel) { + val emitLogLevel = LogLevel.noisiestOf(newLogLevel, oldLogLevel) + log(null, emitLogLevel, "Log level changed to $newLogLevel (was $oldLogLevel)") + } + } + .launchIn(coroutineScope) + } } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/LogLevelUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/LogLevelUnitTest.kt new file mode 100644 index 00000000000..20073b7556d --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/LogLevelUnitTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Google LLC + * + * 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.google.firebase.dataconnect + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoggerUnitTest { + + @Test + fun `noisiestOf()`() = runTest { + assertSoftly { + verifyNoisiestOf(LogLevel.NONE, LogLevel.NONE, LogLevel.NONE) + verifyNoisiestOf(LogLevel.NONE, LogLevel.WARN, LogLevel.WARN) + verifyNoisiestOf(LogLevel.NONE, LogLevel.DEBUG, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.WARN, LogLevel.NONE, LogLevel.WARN) + verifyNoisiestOf(LogLevel.WARN, LogLevel.WARN, LogLevel.WARN) + verifyNoisiestOf(LogLevel.WARN, LogLevel.DEBUG, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.DEBUG, LogLevel.NONE, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.DEBUG, LogLevel.WARN, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.DEBUG, LogLevel.DEBUG, LogLevel.DEBUG) + } + } + + private companion object { + + fun verifyNoisiestOf(logLevel1: LogLevel, logLevel2: LogLevel, expected: LogLevel) { + withClue("noisiestOf($logLevel1, $logLevel2)") { + LogLevel.noisiestOf(logLevel1, logLevel2) shouldBe expected + } + } + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt index 67bbc00b4d2..91a78ff51f1 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectLogLevelRule.kt @@ -24,17 +24,17 @@ import org.junit.rules.ExternalResource * A JUnit test rule that sets the Firebase Data Connect log level to the desired level, then * restores it upon completion of the test. */ -class DataConnectLogLevelRule(val logLevelDuringTest: LogLevel? = LogLevel.DEBUG) : +class DataConnectLogLevelRule(val logLevelDuringTest: LogLevel = LogLevel.DEBUG) : ExternalResource() { private lateinit var logLevelBefore: LogLevel override fun before() { - logLevelBefore = FirebaseDataConnect.logLevel - logLevelDuringTest?.also { FirebaseDataConnect.logLevel = it } + logLevelBefore = FirebaseDataConnect.logLevel.value + logLevelDuringTest.also { FirebaseDataConnect.logLevel.value = it } } override fun after() { - FirebaseDataConnect.logLevel = logLevelBefore + FirebaseDataConnect.logLevel.value = logLevelBefore } }