From dab06b0ed4a3ef12a721b77ed759f986cacc2fa1 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Wed, 26 Feb 2025 10:04:24 -0500 Subject: [PATCH 1/2] [PM-14303] Update Bitwarden SDK and load `bitwarden_uniffi` on older Android versions The Bitwarden SDK dependency was updated to version 1.0.0-20250225.125021-120. The SDK requires access to Android APIs that were made public in API 31 in order to generate email aliases. To address this limitation for devices with earlier API versions, the `bitwarden_uniffi` library is now loaded manually before initializing any `Client` instance. --- .../platform/manager/SdkClientManagerImpl.kt | 17 +++++++++++++++++ gradle/libs.versions.toml | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) 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..c7f0e83281 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,6 +1,9 @@ package com.x8bit.bitwarden.data.platform.manager +import android.os.Build import com.bitwarden.sdk.Client +import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow +import timber.log.Timber /** * Primary implementation of [SdkClientManager]. @@ -15,6 +18,20 @@ 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)) { + @Suppress("TooGenericExceptionCaught") + try { + System.loadLibrary("bitwarden_uniffi") + } catch (e: Exception) { + Timber.e(e, "Failed to load bitwarden_uniffi library.") + } + } + } + override suspend fun getOrCreateClient( userId: String?, ): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() } 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" From 5c076871abaa7e58c37c3088a6bf3ad58b3dabfa Mon Sep 17 00:00:00 2001 From: Patrick Honkonen Date: Wed, 26 Feb 2025 12:28:28 -0500 Subject: [PATCH 2/2] Implement NativeLibraryManager and NativeLibraryManagerImpl for loading native libraries This commit introduces the `NativeLibraryManager` interface and its implementation, `NativeLibraryManagerImpl`. - `NativeLibraryManager` defines a method `loadLibrary` for loading native libraries. - `NativeLibraryManagerImpl` uses `System.loadLibrary` to load libraries and handles potential `UnsatisfiedLinkError`. - `PlatformManagerModule` is updated to provide `NativeLibraryManager` instance and to inject it into `SdkClientManager`. --- .../platform/manager/NativeLibraryManager.kt | 12 +++++ .../manager/NativeLibraryManagerImpl.kt | 20 ++++++++ .../platform/manager/SdkClientManagerImpl.kt | 9 +--- .../manager/di/PlatformManagerModule.kt | 8 ++++ .../platform/manager/SdkClientManagerTest.kt | 47 +++++++++++++++++-- 5 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManager.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/manager/NativeLibraryManagerImpl.kt 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 c7f0e83281..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 @@ -3,13 +3,13 @@ package com.x8bit.bitwarden.data.platform.manager import android.os.Build import com.bitwarden.sdk.Client import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow -import timber.log.Timber /** * 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) @@ -23,12 +23,7 @@ class SdkClientManagerImpl( // to work around this limitation the SDK must be manually loaded prior to initializing any // [Client] instance. if (isBuildVersionBelow(Build.VERSION_CODES.S)) { - @Suppress("TooGenericExceptionCaught") - try { - System.loadLibrary("bitwarden_uniffi") - } catch (e: Exception) { - Timber.e(e, "Failed to load bitwarden_uniffi library.") - } + nativeLibraryManager.loadLibrary("bitwarden_uniffi") } } 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(), + ) }