diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt index 7b6987e77a..3cd29ed8ba 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt @@ -7,8 +7,10 @@ import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.data.manager.NativeLibraryManager import com.bitwarden.sdk.Client import com.bitwarden.sdk.ManagedSettingsBindingClient +import com.bitwarden.sdk.initLogger import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory +import com.x8bit.bitwarden.data.platform.manager.sdk.log.SdkLoggerFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async @@ -17,11 +19,13 @@ import kotlinx.coroutines.runBlocking /** * Primary implementation of [SdkClientManager]. */ +@Suppress("LongParameterList") internal class SdkClientManagerImpl( nativeLibraryManager: NativeLibraryManager, dispatcherManager: DispatcherManager, sdkRepoFactory: SdkRepositoryFactory, sdkPlatformApiFactory: SdkPlatformApiFactory, + sdkLoggerFactory: SdkLoggerFactory, private val featureFlagManager: FeatureFlagManager, private val clientProvider: suspend ( userId: String?, @@ -63,6 +67,11 @@ internal class SdkClientManagerImpl( if (!isBuildVersionAtLeast(Build.VERSION_CODES.S)) { nativeLibraryManager.loadLibrary("bitwarden_uniffi") } + initLogger( + callback = sdkLoggerFactory.getLogCallback(), + level = sdkLoggerFactory.logLevel, + platformLogger = false, + ) // Initialize this now, so that we can access it synchronously later on. globalClientDeferred = ioScope.async { clientProvider(null, null) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index dd746b1b7b..e206369e3e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.manager.di import android.app.Application import android.content.Context import androidx.core.content.getSystemService +import com.bitwarden.core.data.manager.BuildInfoManager import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.bitwarden.core.data.manager.dispatcher.DispatcherManagerImpl import com.bitwarden.core.data.manager.realtime.RealtimeManager @@ -93,6 +94,8 @@ import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactoryImpl import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactoryImpl +import com.x8bit.bitwarden.data.platform.manager.sdk.log.SdkLoggerFactory +import com.x8bit.bitwarden.data.platform.manager.sdk.log.SdkLoggerFactoryImpl import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorImpl import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository @@ -241,12 +244,14 @@ object PlatformManagerModule { nativeLibraryManager: NativeLibraryManager, sdkRepositoryFactory: SdkRepositoryFactory, sdkPlatformApiFactory: SdkPlatformApiFactory, + sdkLoggerFactory: SdkLoggerFactory, ): SdkClientManager = SdkClientManagerImpl( dispatcherManager = dispatcherManager, featureFlagManager = featureFlagManager, nativeLibraryManager = nativeLibraryManager, sdkRepoFactory = sdkRepositoryFactory, sdkPlatformApiFactory = sdkPlatformApiFactory, + sdkLoggerFactory = sdkLoggerFactory, ) @Provides @@ -436,6 +441,14 @@ object PlatformManagerModule { serverCommConfigManager = serverCommConfigManager, ) + @Provides + @Singleton + fun provideSdkLoggerFactory( + buildInfoManager: BuildInfoManager, + ): SdkLoggerFactory = SdkLoggerFactoryImpl( + buildInfoManager = buildInfoManager, + ) + @Provides @Singleton fun provideKeyManager( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactory.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactory.kt new file mode 100644 index 0000000000..a5d441e6de --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactory.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.platform.manager.sdk.log + +import com.bitwarden.sdk.LogCallback +import com.bitwarden.sdk.LogLevel + +/** + * Creates and manages the logger for the Bitwarden SDK. + */ +interface SdkLoggerFactory { + /** + * The log level to be used with the Bitwarden SDK. + */ + val logLevel: LogLevel + + /** + * The [LogCallback] to be used with the Bitwarden SDK. + */ + fun getLogCallback(): LogCallback +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactoryImpl.kt new file mode 100644 index 0000000000..d74c112b93 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactoryImpl.kt @@ -0,0 +1,34 @@ +package com.x8bit.bitwarden.data.platform.manager.sdk.log + +import android.util.Log +import com.bitwarden.core.data.manager.BuildInfoManager +import com.bitwarden.sdk.LogCallback +import com.bitwarden.sdk.LogLevel +import timber.log.Timber + +/** + * The default implementation for the [SdkLoggerFactory]. + */ +internal class SdkLoggerFactoryImpl( + private val buildInfoManager: BuildInfoManager, +) : SdkLoggerFactory { + override val logLevel: LogLevel + get() = if (buildInfoManager.isDevBuild) LogLevel.DEBUG else LogLevel.INFO + + override fun getLogCallback(): LogCallback = object : LogCallback { + override fun onLog(level: LogLevel, target: String, message: String) { + Timber + .tag(tag = "BitwardenSdkClient") + .log(priority = level.logLevel, message = "$target --$message") + } + } +} + +private val LogLevel.logLevel: Int + get() = when (this) { + LogLevel.TRACE -> Log.VERBOSE + LogLevel.DEBUG -> Log.DEBUG + LogLevel.INFO -> Log.INFO + LogLevel.WARN -> Log.WARN + LogLevel.ERROR -> Log.ERROR + } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt index bd18dec554..87a8056b64 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/ScopedVaultSdkSourceImpl.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory +import com.x8bit.bitwarden.data.platform.manager.sdk.log.SdkLoggerFactory /** * The default instance of the [ScopedVaultSdkSource]. This uses its own instance of the @@ -19,6 +20,7 @@ class ScopedVaultSdkSourceImpl( featureFlagManager: FeatureFlagManager, sdkRepositoryFactory: SdkRepositoryFactory, sdkPlatformApiFactory: SdkPlatformApiFactory, + sdkLoggerFactory: SdkLoggerFactory, vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl( sdkClientManager = SdkClientManagerImpl( dispatcherManager = dispatcherManager, @@ -30,6 +32,7 @@ class ScopedVaultSdkSourceImpl( sdkRepoFactory = sdkRepositoryFactory, featureFlagManager = featureFlagManager, sdkPlatformApiFactory = sdkPlatformApiFactory, + sdkLoggerFactory = sdkLoggerFactory, ), dispatcherManager = dispatcherManager, ), diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt index a9ffb99c7b..5a4d95927e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.SdkClientManager import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory +import com.x8bit.bitwarden.data.platform.manager.sdk.log.SdkLoggerFactory import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSourceImpl import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource @@ -43,12 +44,14 @@ object VaultSdkModule { featureFlagManager: FeatureFlagManager, sdkRepositoryFactory: SdkRepositoryFactory, sdkPlatformApiFactory: SdkPlatformApiFactory, + sdkLoggerFactory: SdkLoggerFactory, ): ScopedVaultSdkSource = ScopedVaultSdkSourceImpl( dispatcherManager = dispatcherManager, featureFlagManager = featureFlagManager, sdkRepositoryFactory = sdkRepositoryFactory, sdkPlatformApiFactory = sdkPlatformApiFactory, + sdkLoggerFactory = sdkLoggerFactory, ) @Provides diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt index b974b355f6..78532cb0c0 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt @@ -3,11 +3,16 @@ package com.x8bit.bitwarden.data.platform.manager import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.data.manager.NativeLibraryManager +import com.bitwarden.sdk.LogLevel +import com.bitwarden.sdk.initLogger import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory +import com.x8bit.bitwarden.data.platform.manager.sdk.log.SdkLoggerFactory import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.test.runTest @@ -28,16 +33,21 @@ class SdkClientManagerTest { private val sdkPlatformApiFactory: SdkPlatformApiFactory = mockk { every { getServerCommunicationConfigPlatformApi() } returns mockk() } + private val sdkLoggerFactory: SdkLoggerFactory = mockk { + every { logLevel } returns LogLevel.INFO + every { getLogCallback() } returns mockk() + } @BeforeEach fun setUp() { - mockkStatic(::isBuildVersionAtLeast) - every { isBuildVersionAtLeast(any()) } returns true + mockkStatic(::isBuildVersionAtLeast, ::initLogger) + every { isBuildVersionAtLeast(version = any()) } returns true + every { initLogger(callback = any(), level = any(), platformLogger = any()) } just runs } @AfterEach fun tearDown() { - unmockkStatic(::isBuildVersionAtLeast) + unmockkStatic(::isBuildVersionAtLeast, ::initLogger) } @Test @@ -106,5 +116,6 @@ class SdkClientManagerTest { featureFlagManager = mockk(), sdkRepoFactory = sdkRepoFactory, sdkPlatformApiFactory = sdkPlatformApiFactory, + sdkLoggerFactory = sdkLoggerFactory, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactoryTest.kt new file mode 100644 index 0000000000..3ea7c7876f --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/log/SdkLoggerFactoryTest.kt @@ -0,0 +1,103 @@ +package com.x8bit.bitwarden.data.platform.manager.sdk.log + +import android.util.Log +import com.bitwarden.core.data.manager.BuildInfoManager +import com.bitwarden.sdk.LogLevel +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import timber.log.Timber + +class SdkLoggerFactoryTest { + private val buildInfoManager: BuildInfoManager = mockk { + every { isDevBuild } returns false + } + private val recordingTree = RecordingTree() + + private val sdkLoggerFactory: SdkLoggerFactory = SdkLoggerFactoryImpl( + buildInfoManager = buildInfoManager, + ) + + @BeforeEach + fun setup() { + Timber.plant(recordingTree) + } + + @AfterEach + fun tearDown() { + Timber.uproot(recordingTree) + } + + @Test + fun `logLevel should be DEBUG when the build is a dev build`() { + every { buildInfoManager.isDevBuild } returns true + + assertEquals(LogLevel.DEBUG, sdkLoggerFactory.logLevel) + } + + @Test + fun `logLevel should be INFO when the build is not a dev build`() { + every { buildInfoManager.isDevBuild } returns false + + assertEquals(LogLevel.INFO, sdkLoggerFactory.logLevel) + } + + @Test + fun `getLogCallback should return a callback that logs with the SDK tag`() { + sdkLoggerFactory + .getLogCallback() + .onLog( + level = LogLevel.INFO, + target = "bitwarden_core::target", + message = "mockMessage", + ) + + assertEquals( + listOf( + LogEntry( + priority = Log.INFO, + tag = "BitwardenSdkClient", + message = "bitwarden_core::target --mockMessage", + ), + ), + recordingTree.logEntries, + ) + } + + @Test + fun `getLogCallback should return a callback that maps each SDK level to a log priority`() { + val callback = sdkLoggerFactory.getLogCallback() + + LogLevel.entries.forEach { + callback.onLog(level = it, target = "mockTarget", message = "mockMessage") + } + + assertEquals( + listOf(Log.VERBOSE, Log.DEBUG, Log.INFO, Log.WARN, Log.ERROR), + recordingTree.logEntries.map { it.priority }, + ) + } +} + +/** + * A [Timber.Tree] that records each log for later verification. + */ +private class RecordingTree : Timber.Tree() { + val logEntries: MutableList = mutableListOf() + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + logEntries.add(LogEntry(priority = priority, tag = tag, message = message)) + } +} + +/** + * Models a single log received by the [RecordingTree]. + */ +private data class LogEntry( + val priority: Int, + val tag: String?, + val message: String, +)