[PM-14303] Update Bitwarden SDK and load bitwarden_uniffi on older Android versions (#4793)

This commit is contained in:
Patrick Honkonen
2025-02-27 15:23:55 +00:00
committed by GitHub
6 changed files with 96 additions and 5 deletions

View File

@@ -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<Unit>
}

View File

@@ -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<Unit> {
return try {
System.loadLibrary(libraryName)
Result.success(Unit)
} catch (e: UnsatisfiedLinkError) {
Timber.e(e, "Failed to load native library $libraryName.")
Result.failure(e)
}
}
}

View File

@@ -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<String?, Client>()
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() }

View File

@@ -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

View File

@@ -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<NativeLibraryManager> {
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(),
)
}

View File

@@ -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"