PM-12594 Add observable flows to observe badge count changes when corresponding settings update (#3964)

This commit is contained in:
Dave Severns 2024-09-25 15:37:58 -04:00 committed by GitHub
parent ad154b6c91
commit 40dd0e9776
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 329 additions and 37 deletions

View file

@ -271,6 +271,11 @@ interface SettingsDiskSource {
*/
fun storeShowAutoFillSettingBadge(userId: String, showBadge: Boolean?)
/**
* Emits updates that track [getShowAutoFillSettingBadge] for the given [userId].
*/
fun getShowAutoFillSettingBadgeFlow(userId: String): Flow<Boolean?>
/**
* Gets whether or not the given [userId] has signalled they want to enable unlock options
* later, during onboarding.
@ -282,4 +287,9 @@ interface SettingsDiskSource {
* set up unlock options later, during onboarding.
*/
fun storeShowUnlockSettingBadge(userId: String, showBadge: Boolean?)
/**
* Emits updates that track [getShowUnlockSettingBadge] for the given [userId].
*/
fun getShowUnlockSettingBadgeFlow(userId: String): Flow<Boolean?>
}

View file

@ -58,6 +58,12 @@ class SettingsDiskSourceImpl(
private val mutablePullToRefreshEnabledFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableShowAutoFillSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableShowUnlockSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableIsIconLoadingDisabledFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableIsCrashLoggingEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
@ -334,39 +340,6 @@ class SettingsDiskSourceImpl(
)
}
private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =
mutableLastSyncFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultTimeoutActionFlow(
userId: String,
): MutableSharedFlow<VaultTimeoutAction?> =
mutableVaultTimeoutActionFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultTimeoutInMinutesFlow(
userId: String,
): MutableSharedFlow<Int?> =
mutableVaultTimeoutInMinutesFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutablePullToRefreshEnabledFlowMap(
userId: String,
): MutableSharedFlow<Boolean?> =
mutablePullToRefreshEnabledFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> =
mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
override fun getScreenCaptureAllowed(userId: String): Boolean? {
return getBoolean(key = SCREEN_CAPTURE_ALLOW_KEY.appendIdentifier(userId))
}
@ -403,20 +376,76 @@ class SettingsDiskSourceImpl(
key = SHOW_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
)
override fun storeShowAutoFillSettingBadge(userId: String, showBadge: Boolean?) =
override fun storeShowAutoFillSettingBadge(userId: String, showBadge: Boolean?) {
putBoolean(
key = SHOW_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
value = showBadge,
)
getMutableShowAutoFillSettingBadgeFlow(userId).tryEmit(showBadge)
}
override fun getShowAutoFillSettingBadgeFlow(userId: String): Flow<Boolean?> =
getMutableShowAutoFillSettingBadgeFlow(userId)
.onSubscription { emit(getShowAutoFillSettingBadge(userId)) }
override fun getShowUnlockSettingBadge(userId: String): Boolean? =
getBoolean(
key = SHOW_UNLOCK_SETTING_BADGE.appendIdentifier(userId),
)
override fun storeShowUnlockSettingBadge(userId: String, showBadge: Boolean?) =
override fun storeShowUnlockSettingBadge(userId: String, showBadge: Boolean?) {
putBoolean(
key = SHOW_UNLOCK_SETTING_BADGE.appendIdentifier(userId),
value = showBadge,
)
getMutableShowUnlockSettingBadgeFlow(userId).tryEmit(showBadge)
}
override fun getShowUnlockSettingBadgeFlow(userId: String): Flow<Boolean?> =
getMutableShowUnlockSettingBadgeFlow(userId = userId)
.onSubscription { emit(getShowUnlockSettingBadge(userId)) }
private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =
mutableLastSyncFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultTimeoutActionFlow(
userId: String,
): MutableSharedFlow<VaultTimeoutAction?> =
mutableVaultTimeoutActionFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultTimeoutInMinutesFlow(
userId: String,
): MutableSharedFlow<Int?> =
mutableVaultTimeoutInMinutesFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutablePullToRefreshEnabledFlowMap(
userId: String,
): MutableSharedFlow<Boolean?> =
mutablePullToRefreshEnabledFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> =
mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowAutoFillSettingBadgeFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowUnlockSettingBadgeFlow(userId: String): MutableSharedFlow<Boolean?> =
mutableShowUnlockSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
}

View file

@ -160,6 +160,24 @@ interface SettingsRepository {
*/
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
/**
* Returns an observable count of the number of settings items that have a badge to display
* for the current active user.
*/
val allSettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns an observable count of the number of security settings items that have a badge to
* display for the current active user.
*/
val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns an observable count of the number of autofill settings items that have a badge to
* display for the current active user.
*/
val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
/**
* Disables autofill if it is currently enabled.
*/
@ -278,4 +296,14 @@ interface SettingsRepository {
* set up unlock options later, during onboarding.
*/
fun storeShowUnlockSettingBadge(userId: String, showBadge: Boolean)
/**
* Gets whether or not the given [userId] has signalled they want to enable autofill
*/
fun getShowAutofillBadgeFlow(userId: String): Flow<Boolean>
/**
* Gets whether or not the given [userId] has signalled they want to enable unlock options
*/
fun getShowUnlockBadgeFlow(userId: String): Flow<Boolean>
}

View file

@ -6,6 +6,7 @@ import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
@ -29,6 +30,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
@ -337,6 +340,60 @@ class SettingsRepositoryImpl(
?: DEFAULT_IS_SCREEN_CAPTURE_ALLOWED,
)
override val allSettingsBadgeCountFlow: StateFlow<Int>
get() = combine(
allSecuritySettingsBadgeCountFlow,
allAutofillSettingsBadgeCountFlow,
transform = ::sumSettingsBadgeCount,
)
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
// can be expanded to support multiple security settings
getShowUnlockBadgeFlow(userId = it)
.map { showUnlockBadge ->
listOf(showUnlockBadge)
}
.map { list ->
list.count { badgeOnValue -> badgeOnValue }
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
// Can be expanded to support multiple autofill settings
getShowAutofillBadgeFlow(userId = it)
.map { showAutofillBadge ->
listOf(showAutofillBadge)
}
.map { list ->
list.count { showBadge -> showBadge }
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
init {
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
@ -552,6 +609,14 @@ class SettingsRepositoryImpl(
settingsDiskSource.storeShowUnlockSettingBadge(userId, showBadge)
}
override fun getShowAutofillBadgeFlow(userId: String): Flow<Boolean> =
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId)
.map { it ?: false }
override fun getShowUnlockBadgeFlow(userId: String): Flow<Boolean> =
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId)
.map { it ?: false }
/**
* If there isn't already one generated, generate a symmetric sync key that would be used
* for communicating via IPC.
@ -595,6 +660,10 @@ class SettingsRepositoryImpl(
}
}
}
// helper function to sum badge counts from different settings sub-menus.
private fun sumSettingsBadgeCount(autoFillBadgeCount: Int, securityBadgeCount: Int) =
autoFillBadgeCount + securityBadgeCount
}
/**

View file

@ -1054,6 +1054,23 @@ class SettingsDiskSourceTest {
assertTrue(settingsDiskSource.getShowAutoFillSettingBadge(userId = mockUserId)!!)
}
@Test
fun `storeShowAutoFillSettingBadge should update the flow value`() = runTest {
val mockUserId = "mockUserId"
settingsDiskSource.storeShowAutoFillSettingBadge(mockUserId, true)
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = mockUserId).test {
// The initial values of the Flow are in sync
assertTrue(awaitItem() ?: false)
assertTrue(awaitItem() ?: false)
// update the value to false
settingsDiskSource.storeShowAutoFillSettingBadge(
userId = mockUserId, false,
)
assertFalse(awaitItem() ?: true)
}
}
@Test
fun `storeShowUnlockSettingBadge should update SharedPreferences`() {
val mockUserId = "mockUserId"
@ -1077,4 +1094,21 @@ class SettingsDiskSourceTest {
assertTrue(settingsDiskSource.getShowUnlockSettingBadge(userId = mockUserId)!!)
}
@Test
fun `storeShowUnlockSettingsBadge should update the flow value`() = runTest {
val mockUserId = "mockUserId"
settingsDiskSource.storeShowUnlockSettingBadge(mockUserId, true)
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = mockUserId).test {
// The initial values of the Flow are in sync
assertTrue(awaitItem() ?: false)
assertTrue(awaitItem() ?: false)
// update the value to false
settingsDiskSource.storeShowUnlockSettingBadge(
userId = mockUserId, false,
)
assertFalse(awaitItem() ?: true)
}
}
}

View file

@ -64,6 +64,12 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private val userShowAutoFillBadge = mutableMapOf<String, Boolean?>()
private val userShowUnlockBadge = mutableMapOf<String, Boolean?>()
private val mutableShowAutoFillSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableShowUnlockSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
override var appLanguage: AppLanguage? = null
override var appTheme: AppTheme
@ -291,23 +297,34 @@ class FakeSettingsDiskSource : SettingsDiskSource {
override fun storeShowAutoFillSettingBadge(userId: String, showBadge: Boolean?) {
userShowAutoFillBadge[userId] = showBadge
getMutableShowAutoFillSettingBadgeFlow(userId).tryEmit(showBadge)
}
override fun getShowAutoFillSettingBadgeFlow(userId: String): Flow<Boolean?> =
getMutableShowAutoFillSettingBadgeFlow(userId = userId).onSubscription {
emit(getShowAutoFillSettingBadge(userId = userId))
}
override fun getShowUnlockSettingBadge(userId: String): Boolean? =
userShowUnlockBadge[userId]
override fun storeShowUnlockSettingBadge(userId: String, showBadge: Boolean?) {
userShowUnlockBadge[userId] = showBadge
getMutableShowUnlockSettingBadgeFlow(userId).tryEmit(showBadge)
}
override fun getShowUnlockSettingBadgeFlow(userId: String): Flow<Boolean?> =
getMutableShowUnlockSettingBadgeFlow(userId = userId).onSubscription {
emit(getShowUnlockSettingBadge(userId = userId))
}
//region Private helper functions
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> {
return mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
}
//region Private helper functions
private fun getMutableLastSyncTimeFlow(
userId: String,
): MutableSharedFlow<Instant?> =
@ -335,6 +352,16 @@ class FakeSettingsDiskSource : SettingsDiskSource {
mutablePullToRefreshEnabledFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowAutoFillSettingBadgeFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowUnlockSettingBadgeFlow(userId: String): MutableSharedFlow<Boolean?> =
mutableShowUnlockSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
//endregion Private helper functions
}

View file

@ -1202,6 +1202,101 @@ class SettingsRepositoryTest {
fakeSettingsDiskSource.storeShowUnlockSettingBadge(userId = userId, showBadge = true)
assertTrue(settingsRepository.getShowUnlockSettingBadge(userId = userId))
}
@Suppress("MaxLineLength")
@Test
fun `getShowAutoFillBadgeFlow should emit the values saved to disk and update when they change`() =
runTest {
val userId = "userId"
settingsRepository.getShowAutofillBadgeFlow(userId).test {
assertFalse(awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = userId,
showBadge = true,
)
assertTrue(awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `getShowUnlockBadgeFlow should emit the values saved to disk and update when they change`() =
runTest {
val userId = "userId"
settingsRepository.getShowUnlockBadgeFlow(userId).test {
assertFalse(awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = userId,
showBadge = true,
)
assertTrue(awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `allAutoFillSettingsBadgeCountFlow should emit the value of flags set to true and update when changed`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
settingsRepository.allAutofillSettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = false,
)
assertEquals(0, awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `allSecuritySettingsBadgeCountFlow should emit the value of flags set to true and update when changed`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
settingsRepository.allSecuritySettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = false,
)
assertEquals(0, awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `allSettingsBadgeCountFlow should emit the value of all flags set to true and update when changed`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
settingsRepository.allSettingsBadgeCountFlow.test {
assertEquals(0, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(1, awaitItem())
fakeSettingsDiskSource.storeShowUnlockSettingBadge(
userId = USER_ID,
showBadge = true,
)
assertEquals(2, awaitItem())
fakeSettingsDiskSource.storeShowAutoFillSettingBadge(
userId = USER_ID,
showBadge = false,
)
assertEquals(1, awaitItem())
}
}
}
private const val USER_ID: String = "userId"