mirror of
https://github.com/bitwarden/android.git
synced 2024-11-26 19:36:18 +03:00
BIT-805: Screen capture toggle setting implementation (#788)
This commit is contained in:
parent
c765de99f1
commit
bc834fee93
11 changed files with 189 additions and 11 deletions
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -10,10 +11,13 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -32,6 +36,8 @@ class MainActivity : AppCompatActivity() {
|
|||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
|
@ -67,4 +73,25 @@ class MainActivity : AppCompatActivity() {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
mainViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is MainEvent.ScreenCaptureSettingChange -> {
|
||||
handleScreenCaptureSettingChange(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun handleScreenCaptureSettingChange(event: MainEvent.ScreenCaptureSettingChange) {
|
||||
if (event.isAllowed) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class MainViewModel @Inject constructor(
|
|||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<MainState, Unit, MainAction>(
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
),
|
||||
|
@ -52,6 +52,13 @@ class MainViewModel @Inject constructor(
|
|||
.appThemeStateFlow
|
||||
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.isScreenCaptureAllowedStateFlow
|
||||
.onEach { isAllowed ->
|
||||
sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed))
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: MainAction) {
|
||||
|
@ -133,3 +140,14 @@ sealed class MainAction {
|
|||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events that are emitted by the [MainViewModel].
|
||||
*/
|
||||
sealed class MainEvent {
|
||||
|
||||
/**
|
||||
* Event indicating a change in the screen capture setting.
|
||||
*/
|
||||
data class ScreenCaptureSettingChange(val isAllowed: Boolean) : MainEvent()
|
||||
}
|
||||
|
|
|
@ -177,6 +177,11 @@ interface SettingsDiskSource {
|
|||
*/
|
||||
fun getScreenCaptureAllowed(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Emits updates that track [getScreenCaptureAllowed] for the given [userId].
|
||||
*/
|
||||
fun getScreenCaptureAllowedFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores whether or not [isScreenCaptureAllowed] for the given [userId].
|
||||
*/
|
||||
|
|
|
@ -54,6 +54,9 @@ class SettingsDiskSourceImpl(
|
|||
private val mutableIsIconLoadingDisabledFlow =
|
||||
bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
override var appLanguage: AppLanguage?
|
||||
get() = getString(key = APP_LANGUAGE_KEY)
|
||||
?.let { storedValue ->
|
||||
|
@ -260,6 +263,11 @@ class SettingsDiskSourceImpl(
|
|||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> =
|
||||
mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
override fun getApprovePasswordlessLoginsEnabled(userId: String): Boolean? {
|
||||
return getBoolean(key = "${APPROVE_PASSWORDLESS_LOGINS_KEY}_$userId")
|
||||
}
|
||||
|
@ -278,6 +286,10 @@ class SettingsDiskSourceImpl(
|
|||
return getBoolean(key = "${SCREEN_CAPTURE_ALLOW_KEY}_$userId")
|
||||
}
|
||||
|
||||
override fun getScreenCaptureAllowedFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableScreenCaptureAllowedFlow(userId)
|
||||
.onSubscription { emit(getScreenCaptureAllowed(userId)) }
|
||||
|
||||
override fun storeScreenCaptureAllowed(
|
||||
userId: String,
|
||||
isScreenCaptureAllowed: Boolean?,
|
||||
|
@ -286,5 +298,6 @@ class SettingsDiskSourceImpl(
|
|||
key = "${SCREEN_CAPTURE_ALLOW_KEY}_$userId",
|
||||
value = isScreenCaptureAllowed,
|
||||
)
|
||||
getMutableScreenCaptureAllowedFlow(userId).tryEmit(isScreenCaptureAllowed)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,6 +98,16 @@ interface SettingsRepository {
|
|||
*/
|
||||
val isAutofillEnabledStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Sets whether or not screen capture is allowed for the current user.
|
||||
*/
|
||||
var isScreenCaptureAllowed: Boolean
|
||||
|
||||
/**
|
||||
* Whether or not screen capture is allowed for the current user.
|
||||
*/
|
||||
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Disables autofill if it is currently enabled.
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
|
@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -26,6 +28,8 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
|
||||
private val DEFAULT_IS_SCREEN_CAPTURE_ALLOWED = BuildConfig.DEBUG
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsRepository].
|
||||
*/
|
||||
|
@ -203,6 +207,39 @@ class SettingsRepositoryImpl(
|
|||
override val isAutofillEnabledStateFlow: StateFlow<Boolean> =
|
||||
mutableIsAutofillEnabledStateFlow.asStateFlow()
|
||||
|
||||
override var isScreenCaptureAllowed: Boolean
|
||||
get() = activeUserId?.let {
|
||||
settingsDiskSource.getScreenCaptureAllowed(it)
|
||||
} ?: DEFAULT_IS_SCREEN_CAPTURE_ALLOWED
|
||||
set(value) {
|
||||
val userId = activeUserId ?: return
|
||||
settingsDiskSource.storeScreenCaptureAllowed(
|
||||
userId = userId,
|
||||
isScreenCaptureAllowed = value,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
|
||||
get() = authDiskSource
|
||||
.userStateFlow
|
||||
.flatMapLatest { userState ->
|
||||
userState
|
||||
?.activeUserId
|
||||
?.let {
|
||||
settingsDiskSource.getScreenCaptureAllowedFlow(userId = it)
|
||||
.map { isAllowed -> isAllowed ?: DEFAULT_IS_SCREEN_CAPTURE_ALLOWED }
|
||||
}
|
||||
?: flowOf(DEFAULT_IS_SCREEN_CAPTURE_ALLOWED)
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = activeUserId
|
||||
?.let { settingsDiskSource.getScreenCaptureAllowed(userId = it) }
|
||||
?: DEFAULT_IS_SCREEN_CAPTURE_ALLOWED,
|
||||
)
|
||||
|
||||
init {
|
||||
observeAutofillEnabledChanges()
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ class OtherViewModel @Inject constructor(
|
|||
) : BaseViewModel<OtherState, OtherEvent, OtherAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: OtherState(
|
||||
allowScreenCapture = false,
|
||||
allowScreenCapture = settingsRepo.isScreenCaptureAllowed,
|
||||
allowSyncOnRefresh = settingsRepo.getPullToRefreshEnabledFlow().value,
|
||||
clearClipboardFrequency = OtherState.ClearClipboardFrequency.DEFAULT,
|
||||
lastSyncTime = settingsRepo
|
||||
|
@ -64,7 +64,7 @@ class OtherViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleAllowScreenCaptureToggled(action: OtherAction.AllowScreenCaptureToggle) {
|
||||
// TODO BIT-805 implement screen capture setting
|
||||
settingsRepo.isScreenCaptureAllowed = action.isScreenCaptureEnabled
|
||||
mutableStateFlow.update { it.copy(allowScreenCapture = action.isScreenCaptureEnabled) }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import app.cash.turbine.test
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
|
@ -16,6 +17,7 @@ import io.mockk.every
|
|||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -24,6 +26,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
|
||||
private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT)
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { activeUserId } returns USER_ID
|
||||
|
@ -31,6 +34,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { appTheme } returns AppTheme.DEFAULT
|
||||
every { appThemeStateFlow } returns mutableAppThemeFlow
|
||||
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow
|
||||
}
|
||||
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
|
||||
private val intentManager: IntentManager = mockk {
|
||||
|
@ -143,6 +147,26 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `changes in the allowed screen capture value should result in emissions of ScreenCaptureSettingChange `() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
MainEvent.ScreenCaptureSettingChange(isAllowed = true),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
mutableScreenCaptureAllowedFlow.value = false
|
||||
assertEquals(
|
||||
MainEvent.ScreenCaptureSettingChange(isAllowed = false),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
) = MainViewModel(
|
||||
|
|
|
@ -33,6 +33,9 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
private val mutableIsIconLoadingDisabled =
|
||||
bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private var storedAppTheme: AppTheme = AppTheme.DEFAULT
|
||||
private val storedLastSyncTime = mutableMapOf<String, Instant?>()
|
||||
private val storedVaultTimeoutActions = mutableMapOf<String, VaultTimeoutAction?>()
|
||||
|
@ -193,11 +196,22 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
override fun getScreenCaptureAllowed(userId: String): Boolean? =
|
||||
storedScreenCaptureAllowed[userId]
|
||||
|
||||
override fun getScreenCaptureAllowedFlow(userId: String): Flow<Boolean?> {
|
||||
return getMutableScreenCaptureAllowedFlow(userId)
|
||||
}
|
||||
|
||||
override fun storeScreenCaptureAllowed(
|
||||
userId: String,
|
||||
isScreenCaptureAllowed: Boolean?,
|
||||
) {
|
||||
storedScreenCaptureAllowed[userId] = isScreenCaptureAllowed
|
||||
getMutableScreenCaptureAllowedFlow(userId).tryEmit(isScreenCaptureAllowed)
|
||||
}
|
||||
|
||||
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> {
|
||||
return mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
}
|
||||
|
||||
//region Private helper functions
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class SettingsRepositoryTest {
|
||||
private val autofillManager: AutofillManager = mockk {
|
||||
every { hasEnabledAutofillServices() } answers { isAutofillEnabledAndSupported }
|
||||
|
@ -724,6 +725,29 @@ class SettingsRepositoryTest {
|
|||
fakeSettingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = userId),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isScreenCaptureAllowed property should update SettingsDiskSource and emit changes`() = runTest {
|
||||
val userId = "userId"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
fakeSettingsDiskSource.storeScreenCaptureAllowed(userId, false)
|
||||
|
||||
settingsRepository.isScreenCaptureAllowedStateFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
|
||||
settingsRepository.isScreenCaptureAllowed = true
|
||||
assertTrue(awaitItem())
|
||||
|
||||
assertEquals(true, fakeSettingsDiskSource.getScreenCaptureAllowed(userId))
|
||||
|
||||
settingsRepository.isScreenCaptureAllowed = false
|
||||
assertFalse(awaitItem())
|
||||
|
||||
assertEquals(false, fakeSettingsDiskSource.getScreenCaptureAllowed(userId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MOCK_USER_STATE =
|
||||
|
|
|
@ -27,11 +27,16 @@ class OtherViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
private val mutablePullToRefreshStateFlow = MutableStateFlow(false)
|
||||
private val instant: Instant = Instant.parse("2023-10-26T12:00:00Z")
|
||||
private val isAllowed: Boolean = false
|
||||
private val mutableVaultLastSyncStateFlow = MutableStateFlow(instant)
|
||||
private val mutableScreenCaptureAllowedStateFlow = MutableStateFlow(isAllowed)
|
||||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshStateFlow
|
||||
every { vaultLastSyncStateFlow } returns mutableVaultLastSyncStateFlow
|
||||
every { vaultLastSync } returns instant
|
||||
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedStateFlow
|
||||
every { isScreenCaptureAllowed } answers { mutableScreenCaptureAllowedStateFlow.value }
|
||||
every { isScreenCaptureAllowed = any() } just runs
|
||||
}
|
||||
private val vaultRepository = mockk<VaultRepository>()
|
||||
|
||||
|
@ -50,18 +55,19 @@ class OtherViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(state, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on AllowScreenCaptureToggled should update value in state`() = runTest {
|
||||
fun `on AllowScreenCaptureToggled should update value in state and SettingsRepository`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
expectNoEvents()
|
||||
val newScreenCaptureAllowedValue = true
|
||||
|
||||
viewModel.trySendAction(OtherAction.AllowScreenCaptureToggle(newScreenCaptureAllowedValue))
|
||||
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isScreenCaptureAllowed = newScreenCaptureAllowedValue
|
||||
}
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(OtherAction.AllowScreenCaptureToggle(true))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(allowScreenCapture = true),
|
||||
awaitItem(),
|
||||
|
|
Loading…
Reference in a new issue