mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
[PM-26909] Implement screen capture toggle authenticator (#6033)
This commit is contained in:
parent
8a90d77fd7
commit
3ace095b86
@ -67,16 +67,16 @@ interface SettingsRepository {
|
||||
*/
|
||||
var isScreenCaptureAllowed: Boolean
|
||||
|
||||
/**
|
||||
* A set of Bitwarden account IDs that have previously been synced.
|
||||
*/
|
||||
var previouslySyncedBitwardenAccountIds: Set<String>
|
||||
|
||||
/**
|
||||
* Whether or not screen capture is allowed for the current user.
|
||||
*/
|
||||
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* A set of Bitwarden account IDs that have previously been synced.
|
||||
*/
|
||||
var previouslySyncedBitwardenAccountIds: Set<String>
|
||||
|
||||
/**
|
||||
* Clears any previously stored encrypted user key used with biometrics for the current user.
|
||||
*/
|
||||
|
||||
@ -52,6 +52,7 @@ import com.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
@ -157,6 +158,13 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
},
|
||||
onScreenCaptureChange = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
SettingsAction.SecurityClick.AllowScreenCaptureToggle(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
VaultSettings(
|
||||
@ -256,8 +264,8 @@ private fun SecuritySettings(
|
||||
state: SettingsState,
|
||||
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
|
||||
onBiometricToggle: (Boolean) -> Unit,
|
||||
onScreenCaptureChange: (Boolean) -> Unit,
|
||||
) {
|
||||
if (!biometricsManager.isBiometricsSupported) return
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenListHeaderText(
|
||||
modifier = Modifier
|
||||
@ -265,18 +273,35 @@ private fun SecuritySettings(
|
||||
.padding(horizontal = 16.dp),
|
||||
label = stringResource(id = BitwardenString.security),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
UnlockWithBiometricsRow(
|
||||
val hasBiometrics = biometricsManager.isBiometricsSupported
|
||||
if (hasBiometrics) {
|
||||
UnlockWithBiometricsRow(
|
||||
modifier = Modifier
|
||||
.testTag("UnlockWithBiometricsSwitch")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
isChecked = state.isUnlockWithBiometricsEnabled,
|
||||
onBiometricToggle = { onBiometricToggle(it) },
|
||||
biometricsManager = biometricsManager,
|
||||
)
|
||||
}
|
||||
|
||||
ScreenCaptureRow(
|
||||
currentValue = state.allowScreenCapture,
|
||||
cardStyle = if (hasBiometrics) {
|
||||
CardStyle.Bottom
|
||||
} else {
|
||||
CardStyle.Full
|
||||
},
|
||||
onValueChange = onScreenCaptureChange,
|
||||
modifier = Modifier
|
||||
.testTag("UnlockWithBiometricsSwitch")
|
||||
.fillMaxWidth()
|
||||
.testTag(tag = "AllowScreenCaptureSwitch")
|
||||
.standardHorizontalMargin(),
|
||||
isChecked = state.isUnlockWithBiometricsEnabled,
|
||||
onBiometricToggle = { onBiometricToggle(it) },
|
||||
biometricsManager = biometricsManager,
|
||||
)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Data settings
|
||||
@ -421,7 +446,7 @@ private fun UnlockWithBiometricsRow(
|
||||
var showBiometricsPrompt by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenSwitch(
|
||||
modifier = modifier,
|
||||
cardStyle = CardStyle.Full,
|
||||
cardStyle = CardStyle.Top(),
|
||||
label = stringResource(BitwardenString.unlock_with_biometrics),
|
||||
isChecked = isChecked || showBiometricsPrompt,
|
||||
onCheckedChange = { toggled ->
|
||||
@ -443,6 +468,47 @@ private fun UnlockWithBiometricsRow(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenCaptureRow(
|
||||
currentValue: Boolean,
|
||||
cardStyle: CardStyle,
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var shouldShowScreenCaptureConfirmDialog by remember { mutableStateOf(false) }
|
||||
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = BitwardenString.allow_screen_capture),
|
||||
isChecked = currentValue,
|
||||
onCheckedChange = {
|
||||
if (currentValue) {
|
||||
onValueChange(false)
|
||||
} else {
|
||||
shouldShowScreenCaptureConfirmDialog = true
|
||||
}
|
||||
},
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
if (shouldShowScreenCaptureConfirmDialog) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(BitwardenString.allow_screen_capture),
|
||||
message = stringResource(
|
||||
id = BitwardenString.are_you_sure_you_want_to_enable_screen_capture,
|
||||
),
|
||||
confirmButtonText = stringResource(BitwardenString.yes),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = {
|
||||
onValueChange(true)
|
||||
shouldShowScreenCaptureConfirmDialog = false
|
||||
},
|
||||
onDismissClick = { shouldShowScreenCaptureConfirmDialog = false },
|
||||
onDismissRequest = { shouldShowScreenCaptureConfirmDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Data settings
|
||||
|
||||
//region Appearance settings
|
||||
|
||||
@ -58,6 +58,7 @@ class SettingsViewModel @Inject constructor(
|
||||
accountSyncState = authenticatorBridgeManager.accountSyncStateFlow.value,
|
||||
defaultSaveOption = settingsRepository.defaultSaveOption,
|
||||
sharedAccountsState = authenticatorRepository.sharedCodesStateFlow.value,
|
||||
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
|
||||
),
|
||||
) {
|
||||
|
||||
@ -126,6 +127,10 @@ class SettingsViewModel @Inject constructor(
|
||||
is SettingsAction.SecurityClick.UnlockWithBiometricToggle -> {
|
||||
handleBiometricsSetupClick(action)
|
||||
}
|
||||
|
||||
is SettingsAction.SecurityClick.AllowScreenCaptureToggle -> {
|
||||
handleAllowScreenCaptureToggle(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +178,13 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAllowScreenCaptureToggle(
|
||||
action: SettingsAction.SecurityClick.AllowScreenCaptureToggle,
|
||||
) {
|
||||
settingsRepository.isScreenCaptureAllowed = action.enabled
|
||||
mutableStateFlow.update { it.copy(allowScreenCapture = action.enabled) }
|
||||
}
|
||||
|
||||
private fun handleVaultClick(action: SettingsAction.DataClick) {
|
||||
when (action) {
|
||||
SettingsAction.DataClick.ExportClick -> handleExportClick()
|
||||
@ -319,6 +331,7 @@ class SettingsViewModel @Inject constructor(
|
||||
isSubmitCrashLogsEnabled: Boolean,
|
||||
accountSyncState: AccountSyncState,
|
||||
sharedAccountsState: SharedVerificationCodesState,
|
||||
isScreenCaptureAllowed: Boolean,
|
||||
): SettingsState {
|
||||
val currentYear = Year.now(clock)
|
||||
val copyrightInfo = "© Bitwarden Inc. 2015-$currentYear".asText()
|
||||
@ -343,6 +356,7 @@ class SettingsViewModel @Inject constructor(
|
||||
defaultSaveOption = defaultSaveOption,
|
||||
showSyncWithBitwarden = shouldShowSyncWithBitwarden,
|
||||
showDefaultSaveOptionRow = shouldShowDefaultSaveOption,
|
||||
allowScreenCapture = isScreenCaptureAllowed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -362,6 +376,7 @@ data class SettingsState(
|
||||
val dialog: Dialog?,
|
||||
val version: Text,
|
||||
val copyrightInfo: Text,
|
||||
val allowScreenCapture: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
@ -460,13 +475,18 @@ sealed class SettingsAction(
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the user clicked the Unlock with biometrics button.
|
||||
* Models actions for the Security section of settings.
|
||||
*/
|
||||
sealed class SecurityClick : SettingsAction() {
|
||||
/**
|
||||
* Indicates the user clicked unlock with biometrics toggle.
|
||||
*/
|
||||
data class UnlockWithBiometricToggle(val enabled: Boolean) : SecurityClick()
|
||||
|
||||
/**
|
||||
* Indicates the user clicked allow screen capture toggle.
|
||||
*/
|
||||
data class AllowScreenCaptureToggle(val enabled: Boolean) : SecurityClick()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -20,6 +20,7 @@ import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.bitwarden.ui.util.concat
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
@ -175,6 +176,38 @@ class SettingsScreenTest : AuthenticatorComposeTest() {
|
||||
.onNode(isDialog())
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on allow screen capture confirm should send AllowScreenCaptureToggle`() {
|
||||
composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick()
|
||||
composeTestRule.onNodeWithText("Yes").performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SettingsAction.SecurityClick.AllowScreenCaptureToggle(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on allow screen capture cancel should dismiss dialog`() {
|
||||
composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on allow screen capture row click should display confirm enable screen capture dialog`() {
|
||||
composeTestRule.onNodeWithText("Allow screen capture").performScrollTo().performClick()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Allow screen capture")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
private val APP_LANGUAGE = AppLanguage.ENGLISH
|
||||
@ -194,4 +227,5 @@ private val DEFAULT_STATE = SettingsState(
|
||||
version = BitwardenString.version.asText()
|
||||
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),
|
||||
copyrightInfo = "© Bitwarden Inc. 2015-2024".asText(),
|
||||
allowScreenCapture = false,
|
||||
)
|
||||
|
||||
@ -47,6 +47,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
||||
every { sharedCodesStateFlow } returns mutableSharedCodesFlow
|
||||
}
|
||||
private val mutableDefaultSaveOptionFlow = bufferedMutableSharedFlow<DefaultSaveOption>()
|
||||
private val mutableScreenCaptureAllowedStateFlow = MutableStateFlow(false)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { appLanguage } returns APP_LANGUAGE
|
||||
every { appTheme } returns APP_THEME
|
||||
@ -54,6 +55,9 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
||||
every { defaultSaveOptionFlow } returns mutableDefaultSaveOptionFlow
|
||||
every { isUnlockWithBiometricsEnabled } returns true
|
||||
every { isCrashLoggingEnabled } returns true
|
||||
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedStateFlow
|
||||
every { isScreenCaptureAllowed } answers { mutableScreenCaptureAllowedStateFlow.value }
|
||||
every { isScreenCaptureAllowed = any() } just runs
|
||||
}
|
||||
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||
|
||||
@ -197,6 +201,30 @@ class SettingsViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on AllowScreenCaptureToggled should update value in state and SettingsRepository`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
val newScreenCaptureAllowedValue = true
|
||||
|
||||
viewModel.trySendAction(
|
||||
SettingsAction.SecurityClick.AllowScreenCaptureToggle(
|
||||
newScreenCaptureAllowedValue,
|
||||
),
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isScreenCaptureAllowed = newScreenCaptureAllowedValue
|
||||
}
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(allowScreenCapture = true),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
savedState: SettingsState? = DEFAULT_STATE,
|
||||
) = SettingsViewModel(
|
||||
@ -231,4 +259,5 @@ private val DEFAULT_STATE = SettingsState(
|
||||
version = BitwardenString.version.asText()
|
||||
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),
|
||||
copyrightInfo = "© Bitwarden Inc. 2015-2024".asText(),
|
||||
allowScreenCapture = false,
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user