diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManager.kt new file mode 100644 index 0000000000..47803b0dde --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManager.kt @@ -0,0 +1,12 @@ +package com.x8bit.bitwarden.data.platform.manager + +/** + * Manager for loading native libraries. + */ +interface NativeLibraryManager { + + /** + * Loads a native library with the given [libraryName]. + */ + fun loadLibrary(libraryName: String): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManagerImpl.kt new file mode 100644 index 0000000000..78fd2a5490 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManagerImpl.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.platform.manager + +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +import timber.log.Timber + +/** + * Primary implementation of [NativeLibraryManager]. + */ +@OmitFromCoverage +class NativeLibraryManagerImpl : NativeLibraryManager { + override fun loadLibrary(libraryName: String): Result { + return try { + System.loadLibrary(libraryName) + Result.success(Unit) + } catch (e: UnsatisfiedLinkError) { + Timber.e(e, "Failed to load native library $libraryName.") + Result.failure(e) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt index 456a95eaa8..38e4542f2a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerImpl.kt @@ -1,12 +1,15 @@ package com.x8bit.bitwarden.data.platform.manager +import android.os.Build import com.bitwarden.sdk.Client +import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow /** * Primary implementation of [SdkClientManager]. */ class SdkClientManagerImpl( private val featureFlagManager: FeatureFlagManager, + nativeLibraryManager: NativeLibraryManager, private val clientProvider: suspend () -> Client = { Client(settings = null).apply { platform().loadFlags(featureFlagManager.sdkFeatureFlags) @@ -15,6 +18,15 @@ class SdkClientManagerImpl( ) : SdkClientManager { private val userIdToClientMap = mutableMapOf() + init { + // The SDK requires access to Android APIs that were not made public until API 31. In order + // to work around this limitation the SDK must be manually loaded prior to initializing any + // [Client] instance. + if (isBuildVersionBelow(Build.VERSION_CODES.S)) { + nativeLibraryManager.loadLibrary("bitwarden_uniffi") + } + } + override suspend fun getOrCreateClient( userId: String?, ): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index da3f2f9a64..277cf8d859 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -34,6 +34,8 @@ import com.x8bit.bitwarden.data.platform.manager.KeyManager import com.x8bit.bitwarden.data.platform.manager.KeyManagerImpl import com.x8bit.bitwarden.data.platform.manager.LogsManager import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl +import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager +import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManagerImpl import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl import com.x8bit.bitwarden.data.platform.manager.PushManager @@ -193,12 +195,18 @@ object PlatformManagerModule { ) } + @Provides + @Singleton + fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl() + @Provides @Singleton fun provideSdkClientManager( featureFlagManager: FeatureFlagManager, + nativeLibraryManager: NativeLibraryManager, ): SdkClientManager = SdkClientManagerImpl( featureFlagManager = featureFlagManager, + nativeLibraryManager = nativeLibraryManager, ) @Provides diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt index c66436e3e9..c1e8249033 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/SdkClientManagerTest.kt @@ -1,23 +1,55 @@ package com.x8bit.bitwarden.data.platform.manager +import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow +import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class SdkClientManagerTest { - private val sdkClientManager = SdkClientManagerImpl( - clientProvider = { mockk(relaxed = true) }, - featureFlagManager = mockk(), - ) + private val mockNativeLibraryManager = mockk { + every { loadLibrary(any()) } returns Result.success(Unit) + } + + @BeforeEach + fun setUp() { + mockkStatic(::isBuildVersionBelow) + every { isBuildVersionBelow(any()) } returns false + } + + @AfterEach + fun tearDown() { + unmockkStatic(::isBuildVersionBelow) + } + + @Test + fun `init should load the bitwarden_uniffi library when build version is below 31`() = runTest { + every { isBuildVersionBelow(31) } returns true + createSdkClientManager() + verify { mockNativeLibraryManager.loadLibrary("bitwarden_uniffi") } + } + + @Test + fun `init should not load the bitwarden_uniffi library when build version is 31 or above`() = + runTest { + every { isBuildVersionBelow(31) } returns false + createSdkClientManager() + verify(exactly = 0) { mockNativeLibraryManager.loadLibrary("bitwarden_uniffi") } + } @Suppress("MaxLineLength") @Test fun `getOrCreateClient should create a new client for each userId and return a cached client for subsequent calls`() = runTest { + val sdkClientManager = createSdkClientManager() val userId = "userId" val firstClient = sdkClientManager.getOrCreateClient(userId = userId) @@ -33,6 +65,7 @@ class SdkClientManagerTest { @Test fun `destroyClient should call close on the Client and remove it from the cache`() = runTest { + val sdkClientManager = createSdkClientManager() val userId = "userId" val firstClient = sdkClientManager.getOrCreateClient(userId = userId) @@ -44,4 +77,10 @@ class SdkClientManagerTest { val secondClient = sdkClientManager.getOrCreateClient(userId = userId) assertNotEquals(firstClient, secondClient) } + + private fun createSdkClientManager(): SdkClientManagerImpl = SdkClientManagerImpl( + clientProvider = { mockk(relaxed = true) }, + nativeLibraryManager = mockNativeLibraryManager, + featureFlagManager = mockk(), + ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba3f0b9a9a..56847c971a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ androidxTestRules = "1.6.1" androidXAppCompat = "1.7.0" androdixAutofill = "1.1.0" androidxWork = "2.10.0" -bitwardenSdk = "1.0.0-20250213.181812-113" +bitwardenSdk = "1.0.0-20250225.125021-120" crashlytics = "3.0.3" detekt = "1.23.7" firebaseBom = "33.9.0"