BIT-805: Screen capture toggle setting implementation (#788)

This commit is contained in:
Joshua Queen
2024-01-26 11:26:17 -05:00
committed by Álison Fernandes
parent c765de99f1
commit bc834fee93
11 changed files with 189 additions and 11 deletions

View File

@@ -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)
}
}
}

View File

@@ -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()
}

View File

@@ -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].
*/

View File

@@ -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)
}
}

View File

@@ -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.
*/

View File

@@ -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()
}

View File

@@ -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) }
}