diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt index 2758daff8..af36ed12f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt @@ -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 + /** * 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 } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index e85faacd2..809d10a3d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -58,6 +58,12 @@ class SettingsDiskSourceImpl( private val mutablePullToRefreshEnabledFlowMap = mutableMapOf>() + private val mutableShowAutoFillSettingBadgeFlowMap = + mutableMapOf>() + + private val mutableShowUnlockSettingBadgeFlowMap = + mutableMapOf>() + private val mutableIsIconLoadingDisabledFlow = bufferedMutableSharedFlow() private val mutableIsCrashLoggingEnabledFlow = bufferedMutableSharedFlow() @@ -334,39 +340,6 @@ class SettingsDiskSourceImpl( ) } - private fun getMutableLastSyncFlow( - userId: String, - ): MutableSharedFlow = - mutableLastSyncFlowMap.getOrPut(userId) { - bufferedMutableSharedFlow(replay = 1) - } - - private fun getMutableVaultTimeoutActionFlow( - userId: String, - ): MutableSharedFlow = - mutableVaultTimeoutActionFlowMap.getOrPut(userId) { - bufferedMutableSharedFlow(replay = 1) - } - - private fun getMutableVaultTimeoutInMinutesFlow( - userId: String, - ): MutableSharedFlow = - mutableVaultTimeoutInMinutesFlowMap.getOrPut(userId) { - bufferedMutableSharedFlow(replay = 1) - } - - private fun getMutablePullToRefreshEnabledFlowMap( - userId: String, - ): MutableSharedFlow = - mutablePullToRefreshEnabledFlowMap.getOrPut(userId) { - bufferedMutableSharedFlow(replay = 1) - } - - private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow = - 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 = + 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 = + getMutableShowUnlockSettingBadgeFlow(userId = userId) + .onSubscription { emit(getShowUnlockSettingBadge(userId)) } + + private fun getMutableLastSyncFlow( + userId: String, + ): MutableSharedFlow = + mutableLastSyncFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + + private fun getMutableVaultTimeoutActionFlow( + userId: String, + ): MutableSharedFlow = + mutableVaultTimeoutActionFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + + private fun getMutableVaultTimeoutInMinutesFlow( + userId: String, + ): MutableSharedFlow = + mutableVaultTimeoutInMinutesFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + + private fun getMutablePullToRefreshEnabledFlowMap( + userId: String, + ): MutableSharedFlow = + mutablePullToRefreshEnabledFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + + private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow = + mutableScreenCaptureAllowedFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + + private fun getMutableShowAutoFillSettingBadgeFlow( + userId: String, + ): MutableSharedFlow = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + + private fun getMutableShowUnlockSettingBadgeFlow(userId: String): MutableSharedFlow = + mutableShowUnlockSettingBadgeFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt index 70cfcf3ed..b096dc072 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt @@ -160,6 +160,24 @@ interface SettingsRepository { */ val isScreenCaptureAllowedStateFlow: StateFlow + /** + * Returns an observable count of the number of settings items that have a badge to display + * for the current active user. + */ + val allSettingsBadgeCountFlow: StateFlow + + /** + * 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 + + /** + * 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 + /** * 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 + + /** + * Gets whether or not the given [userId] has signalled they want to enable unlock options + */ + fun getShowUnlockBadgeFlow(userId: String): Flow } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt index f66a6a1bb..2f40a567a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt @@ -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 + get() = combine( + allSecuritySettingsBadgeCountFlow, + allAutofillSettingsBadgeCountFlow, + transform = ::sumSettingsBadgeCount, + ) + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Lazily, + initialValue = 0, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + override val allSecuritySettingsBadgeCountFlow: StateFlow + 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 + 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 = + settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId) + .map { it ?: false } + + override fun getShowUnlockBadgeFlow(userId: String): Flow = + 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 } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt index 3b333eff4..7de051b5d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt @@ -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) + } + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt index 19b28ce62..79c734149 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt @@ -64,6 +64,12 @@ class FakeSettingsDiskSource : SettingsDiskSource { private val userShowAutoFillBadge = mutableMapOf() private val userShowUnlockBadge = mutableMapOf() + private val mutableShowAutoFillSettingBadgeFlowMap = + mutableMapOf>() + + private val mutableShowUnlockSettingBadgeFlowMap = + mutableMapOf>() + 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 = + 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 = + getMutableShowUnlockSettingBadgeFlow(userId = userId).onSubscription { + emit(getShowUnlockSettingBadge(userId = userId)) + } + + //region Private helper functions private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow { return mutableScreenCaptureAllowedFlowMap.getOrPut(userId) { bufferedMutableSharedFlow(replay = 1) } } - //region Private helper functions - private fun getMutableLastSyncTimeFlow( userId: String, ): MutableSharedFlow = @@ -335,6 +352,16 @@ class FakeSettingsDiskSource : SettingsDiskSource { mutablePullToRefreshEnabledFlowMap.getOrPut(userId) { bufferedMutableSharedFlow(replay = 1) } + private fun getMutableShowAutoFillSettingBadgeFlow( + userId: String, + ): MutableSharedFlow = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + + private fun getMutableShowUnlockSettingBadgeFlow(userId: String): MutableSharedFlow = + mutableShowUnlockSettingBadgeFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } //endregion Private helper functions } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 52c5bf030..8a5b1788a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -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"