Skip to content

Commit d81b000

Browse files
authored
[PM-27150] React to device changes on device screen unlock method (#6103)
1 parent 794b27a commit d81b000

File tree

20 files changed

+944
-15
lines changed

20 files changed

+944
-15
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ interface AuthDiskSource : AppIdProvider {
211211
/**
212212
* Gets the flow for the biometrics key for the given [userId].
213213
*/
214-
fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?>
214+
fun getUserBiometricUnlockKeyFlow(userId: String): Flow<String?>
215215

216216
/**
217217
* Retrieves a pin-protected user key for the given [userId].

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ class AuthDiskSourceImpl(
331331
getMutableBiometricUnlockKeyFlow(userId).tryEmit(biometricsKey)
332332
}
333333

334-
override fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?> =
334+
override fun getUserBiometricUnlockKeyFlow(userId: String): Flow<String?> =
335335
getMutableBiometricUnlockKeyFlow(userId)
336336
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
337337

app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ class SettingsRepositoryImpl(
279279
get() = activeUserId
280280
?.let { userId ->
281281
authDiskSource
282-
.getUserBiometicUnlockKeyFlow(userId)
282+
.getUserBiometricUnlockKeyFlow(userId)
283283
.map { it != null }
284284
}
285285
?: flowOf(false)

app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,25 @@ class AuthDiskSourceTest {
721721
assertEquals(biometricsKey, actual)
722722
}
723723

724+
@Test
725+
fun `getUserBiometricUnlockKeyFlow should react to changes in getUserBiometricUnlockKey`() =
726+
runTest {
727+
val mockUserId = "mockUserId"
728+
val biometricsKey = "1234"
729+
authDiskSource.getUserBiometricUnlockKeyFlow(userId = mockUserId).test {
730+
// The initial values of the Flow and the property are in sync
731+
assertNull(authDiskSource.getUserBiometricUnlockKey(userId = mockUserId))
732+
assertNull(awaitItem())
733+
734+
// Updating the disk source updates shared preferences
735+
authDiskSource.storeUserBiometricUnlockKey(
736+
userId = mockUserId,
737+
biometricsKey = biometricsKey,
738+
)
739+
assertEquals(biometricsKey, awaitItem())
740+
}
741+
}
742+
724743
@Test
725744
fun `storeUserBiometricInitVector for non-null values should update SharedPreferences`() {
726745
val biometricsInitVectorBaseKey = "bwSecureStorage:biometricInitializationVector"
@@ -783,11 +802,11 @@ class AuthDiskSourceTest {
783802

784803
@Suppress("MaxLineLength")
785804
@Test
786-
fun `storeUserBiometricUnlockKey should update the resulting flow from getUserBiometicUnlockKeyFlow`() =
805+
fun `storeUserBiometricUnlockKey should update the resulting flow from getUserBiometricUnlockKeyFlow`() =
787806
runTest {
788807
val topSecretKey = "topsecret"
789808
val mockUserId = "mockUserId"
790-
authDiskSource.getUserBiometicUnlockKeyFlow(mockUserId).test {
809+
authDiskSource.getUserBiometricUnlockKeyFlow(mockUserId).test {
791810
assertNull(awaitItem())
792811
authDiskSource.storeUserBiometricUnlockKey(
793812
userId = mockUserId,

app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ class FakeAuthDiskSource : AuthDiskSource {
274274
getMutableBiometricUnlockKeyFlow(userId).tryEmit(biometricsKey)
275275
}
276276

277-
override fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?> =
277+
override fun getUserBiometricUnlockKeyFlow(userId: String): Flow<String?> =
278278
getMutableBiometricUnlockKeyFlow(userId)
279279
.onSubscription { emit(getUserBiometricUnlockKey(userId)) }
280280

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSource.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package com.bitwarden.authenticator.data.auth.datasource.disk
22

33
import com.bitwarden.network.provider.AppIdProvider
4+
import kotlinx.coroutines.flow.Flow
45

56
/**
67
* Primary access point for disk information.
78
*/
89
interface AuthDiskSource : AppIdProvider {
910

11+
/**
12+
* Tracks the biometrics key.
13+
*/
14+
val userBiometricUnlockKeyFlow: Flow<String?>
15+
1016
/**
1117
* Retrieves the "last active time".
1218
*

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/auth/datasource/disk/AuthDiskSourceImpl.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.bitwarden.authenticator.data.auth.datasource.disk
22

33
import android.content.SharedPreferences
4+
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
45
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.onSubscription
58
import java.util.UUID
69

710
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetricKey"
@@ -20,6 +23,7 @@ class AuthDiskSourceImpl(
2023
sharedPreferences = sharedPreferences,
2124
),
2225
AuthDiskSource {
26+
private val mutableUserBiometricUnlockKeyFlow = bufferedMutableSharedFlow<String?>(replay = 1)
2327

2428
override val uniqueAppId: String
2529
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
@@ -39,13 +43,21 @@ class AuthDiskSourceImpl(
3943
override fun getUserBiometricUnlockKey(): String? =
4044
getEncryptedString(key = BIOMETRICS_UNLOCK_KEY)
4145

46+
override val userBiometricUnlockKeyFlow: Flow<String?>
47+
get() =
48+
mutableUserBiometricUnlockKeyFlow
49+
.onSubscription {
50+
emit(getUserBiometricUnlockKey())
51+
}
52+
4253
override fun storeUserBiometricUnlockKey(
4354
biometricsKey: String?,
4455
) {
4556
putEncryptedString(
4657
key = BIOMETRICS_UNLOCK_KEY,
4758
value = biometricsKey,
4859
)
60+
mutableUserBiometricUnlockKeyFlow.tryEmit(biometricsKey)
4961
}
5062

5163
override var authenticatorBridgeSymmetricSyncKey: ByteArray?

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ interface SettingsRepository {
5757
*/
5858
val isUnlockWithBiometricsEnabled: Boolean
5959

60+
/**
61+
* Tracks whether or not biometric unlocking is enabled for the current user.
62+
*/
63+
val isUnlockWithBiometricsEnabledFlow: StateFlow<Boolean>
64+
6065
/**
6166
* Tracks changes to the expiration alert threshold.
6267
*/

authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryImpl.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ class SettingsRepositoryImpl(
6666
override val isUnlockWithBiometricsEnabled: Boolean
6767
get() = authDiskSource.getUserBiometricUnlockKey() != null
6868

69+
override val isUnlockWithBiometricsEnabledFlow: StateFlow<Boolean>
70+
get() =
71+
authDiskSource
72+
.userBiometricUnlockKeyFlow
73+
.map { it != null }
74+
.stateIn(
75+
scope = unconfinedScope,
76+
started = SharingStarted.Eagerly,
77+
initialValue = isUnlockWithBiometricsEnabled,
78+
)
79+
6980
override val appThemeStateFlow: StateFlow<AppTheme>
7081
get() = settingsDiskSource
7182
.appThemeFlow
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.bitwarden.authenticator.ui.platform.components.biometrics
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.lifecycle.Lifecycle
6+
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
7+
import com.bitwarden.ui.platform.base.util.LifecycleEventEffect
8+
9+
/**
10+
* Tracks changes in biometric support and notifies when the app resumes.
11+
*
12+
* This composable monitors lifecycle events and checks biometric support status
13+
* whenever the app returns to the foreground ([Lifecycle.Event.ON_RESUME]) or
14+
* biometric support status changes (via [LaunchedEffect]).
15+
*
16+
* @param biometricsManager Manager to check current biometric support status.
17+
* @param onBiometricSupportChange Callback invoked with the current biometric
18+
* support status.
19+
*/
20+
@Composable
21+
fun BiometricChanges(
22+
biometricsManager: BiometricsManager,
23+
onBiometricSupportChange: (isSupported: Boolean) -> Unit,
24+
) {
25+
LaunchedEffect(biometricsManager.isBiometricsSupported) {
26+
onBiometricSupportChange(biometricsManager.isBiometricsSupported)
27+
}
28+
29+
LifecycleEventEffect { _, event ->
30+
when (event) {
31+
Lifecycle.Event.ON_RESUME -> {
32+
onBiometricSupportChange(biometricsManager.isBiometricsSupported)
33+
}
34+
35+
else -> Unit
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)