From 99b2fd18cbba1a70eefa70a9b84863eff3d7c2f5 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Tue, 21 Oct 2025 19:04:08 +0100 Subject: [PATCH 1/3] Changing screen capture flow from event based to state based on Authenticator --- .../bitwarden/authenticator/MainActivity.kt | 9 ++-- .../bitwarden/authenticator/MainViewModel.kt | 9 ++-- .../authenticator/MainViewModelTest.kt | 43 +++++++++++++++++-- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt index 9f38f2078e5..c40a47012d3 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt @@ -62,6 +62,7 @@ class MainActivity : AppCompatActivity() { setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme }) setContent { val state by mainViewModel.stateFlow.collectAsStateWithLifecycle() + handleScreenCaptureSettingChange(isScreenCaptureAllowed = state.isScreenCaptureAllowed) val navController = rememberNavController() observeViewModelEvents(navController) LocalManagerProvider { @@ -95,10 +96,6 @@ class MainActivity : AppCompatActivity() { .eventFlow .onEach { event -> when (event) { - is MainEvent.ScreenCaptureSettingChange -> { - handleScreenCaptureSettingChange(event) - } - MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen() is MainEvent.UpdateAppTheme -> { AppCompatDelegate.setDefaultNightMode(event.osTheme) @@ -122,8 +119,8 @@ class MainActivity : AppCompatActivity() { mainViewModel.trySendAction(MainAction.OpenDebugMenu) } - private fun handleScreenCaptureSettingChange(event: MainEvent.ScreenCaptureSettingChange) { - if (event.isAllowed) { + private fun handleScreenCaptureSettingChange(isScreenCaptureAllowed: Boolean) { + if (isScreenCaptureAllowed) { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } else { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt index 20ff6ffe7a1..19fda512841 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt @@ -25,6 +25,7 @@ class MainViewModel @Inject constructor( ) : BaseViewModel( MainState( theme = settingsRepository.appTheme, + isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed, ), ) { @@ -37,7 +38,7 @@ class MainViewModel @Inject constructor( settingsRepository .isScreenCaptureAllowedStateFlow .onEach { isAllowed -> - sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed)) + mutableStateFlow.update { it.copy(isScreenCaptureAllowed = isAllowed) } } .launchIn(viewModelScope) viewModelScope.launch { @@ -91,6 +92,7 @@ class MainViewModel @Inject constructor( @Parcelize data class MainState( val theme: AppTheme, + val isScreenCaptureAllowed: Boolean, ) : Parcelable /** @@ -136,11 +138,6 @@ sealed class MainEvent { */ data object NavigateToDebugMenu : MainEvent() - /** - * Event indicating a change in the screen capture setting. - */ - data class ScreenCaptureSettingChange(val isAllowed: Boolean) : MainEvent() - /** * Indicates that the app theme has been updated. */ diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/MainViewModelTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/MainViewModelTest.kt index 51cee18c38c..d5a7d843332 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/MainViewModelTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/MainViewModelTest.kt @@ -21,6 +21,7 @@ class MainViewModelTest : BaseViewModelTest() { every { appTheme } returns AppTheme.DEFAULT every { appThemeStateFlow } returns mutableAppThemeFlow every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow + every { isScreenCaptureAllowed } returns false } private val fakeServerConfigRepository = FakeServerConfigRepository() private val mainViewModel: MainViewModel = MainViewModel( @@ -31,14 +32,20 @@ class MainViewModelTest : BaseViewModelTest() { @Test fun `on AppThemeChanged should update state`() = runTest { mainViewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow -> - eventFlow.skipItems(count = 2) + eventFlow.skipItems(count = 1) assertEquals( - MainState(theme = AppTheme.DEFAULT), + MainState( + theme = AppTheme.DEFAULT, + isScreenCaptureAllowed = false, + ), stateFlow.awaitItem(), ) mainViewModel.trySendAction(MainAction.Internal.ThemeUpdate(theme = AppTheme.DARK)) assertEquals( - MainState(theme = AppTheme.DARK), + MainState( + theme = AppTheme.DARK, + isScreenCaptureAllowed = false, + ), stateFlow.awaitItem(), ) assertEquals( @@ -57,9 +64,37 @@ class MainViewModelTest : BaseViewModelTest() { fun `send NavigateToDebugMenu action when OpenDebugMenu action is sent`() = runTest { mainViewModel.eventFlow.test { // Ignore the events that are fired off by flows in the ViewModel init - skipItems(2) + skipItems(1) mainViewModel.trySendAction(MainAction.OpenDebugMenu) assertEquals(MainEvent.NavigateToDebugMenu, awaitItem()) } } + + @Test + fun `changes in the allowed screen capture value should update the state`() { + val viewModel = createViewModel() + + assertEquals( + MainState( + theme = AppTheme.DEFAULT, + isScreenCaptureAllowed = false, + ), + viewModel.stateFlow.value, + ) + + mutableScreenCaptureAllowedFlow.value = true + + assertEquals( + MainState( + theme = AppTheme.DEFAULT, + isScreenCaptureAllowed = true, + ), + viewModel.stateFlow.value, + ) + } + + private fun createViewModel() = MainViewModel( + settingsRepository = settingsRepository, + configRepository = fakeServerConfigRepository, + ) } From d8905eefbe9ace2217be3ac104c6061f61c7c162 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Wed, 22 Oct 2025 11:17:25 +0100 Subject: [PATCH 2/3] using action to update state on MainViewModel Improved method naming on MainActivity --- .../bitwarden/authenticator/MainActivity.kt | 4 ++-- .../bitwarden/authenticator/MainViewModel.kt | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt index c40a47012d3..9682ab21352 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt @@ -62,7 +62,7 @@ class MainActivity : AppCompatActivity() { setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme }) setContent { val state by mainViewModel.stateFlow.collectAsStateWithLifecycle() - handleScreenCaptureSettingChange(isScreenCaptureAllowed = state.isScreenCaptureAllowed) + updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed) val navController = rememberNavController() observeViewModelEvents(navController) LocalManagerProvider { @@ -119,7 +119,7 @@ class MainActivity : AppCompatActivity() { mainViewModel.trySendAction(MainAction.OpenDebugMenu) } - private fun handleScreenCaptureSettingChange(isScreenCaptureAllowed: Boolean) { + private fun updateScreenCapture(isScreenCaptureAllowed: Boolean) { if (isScreenCaptureAllowed) { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } else { diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt index 19fda512841..9b19a854f09 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt @@ -9,6 +9,7 @@ import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -37,10 +38,10 @@ class MainViewModel @Inject constructor( settingsRepository .isScreenCaptureAllowedStateFlow - .onEach { isAllowed -> - mutableStateFlow.update { it.copy(isScreenCaptureAllowed = isAllowed) } - } + .map { MainAction.Internal.ScreenCaptureUpdate(it) } + .onEach(::trySendAction) .launchIn(viewModelScope) + viewModelScope.launch { configRepository.getServerConfig(forceRefresh = false) } @@ -52,6 +53,10 @@ class MainViewModel @Inject constructor( is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action) is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action) MainAction.OpenDebugMenu -> handleOpenDebugMenu() + + is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate( + isAllowed = action.isScreenCaptureEnabled, + ) } } @@ -78,6 +83,10 @@ class MainViewModel @Inject constructor( ) } + private fun handleScreenCaptureUpdate(isAllowed: Boolean) { + mutableStateFlow.update { it.copy(isScreenCaptureAllowed = isAllowed) } + } + private fun handleIntent( intent: Intent, isFirstIntent: Boolean, @@ -125,6 +134,13 @@ sealed class MainAction { data class ThemeUpdate( val theme: AppTheme, ) : Internal() + + /** + * Indicates that the screen capture state has changed. + */ + data class ScreenCaptureUpdate( + val isScreenCaptureEnabled: Boolean, + ) : Internal() } } From b21c4a7b43a37f2f6d02e94fde4a4ca7c50e2d96 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Thu, 23 Oct 2025 11:52:29 +0100 Subject: [PATCH 3/3] using sendAction and passing whole action to handler --- .../com/bitwarden/authenticator/MainViewModel.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt index 9b19a854f09..c70f2811c6f 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt @@ -39,7 +39,7 @@ class MainViewModel @Inject constructor( settingsRepository .isScreenCaptureAllowedStateFlow .map { MainAction.Internal.ScreenCaptureUpdate(it) } - .onEach(::trySendAction) + .onEach(::sendAction) .launchIn(viewModelScope) viewModelScope.launch { @@ -55,7 +55,7 @@ class MainViewModel @Inject constructor( MainAction.OpenDebugMenu -> handleOpenDebugMenu() is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate( - isAllowed = action.isScreenCaptureEnabled, + screenCaptureUpdateAction = action, ) } } @@ -83,8 +83,14 @@ class MainViewModel @Inject constructor( ) } - private fun handleScreenCaptureUpdate(isAllowed: Boolean) { - mutableStateFlow.update { it.copy(isScreenCaptureAllowed = isAllowed) } + private fun handleScreenCaptureUpdate( + screenCaptureUpdateAction: MainAction.Internal.ScreenCaptureUpdate, + ) { + mutableStateFlow.update { + it.copy( + isScreenCaptureAllowed = screenCaptureUpdateAction.isScreenCaptureEnabled, + ) + } } private fun handleIntent(