Allow screenshots

This commit is contained in:
Rafał Kobyłko 2023-05-15 23:55:59 +02:00
parent 6148592e2b
commit 1e7c48dd9e
23 changed files with 255 additions and 67 deletions

View File

@ -3,6 +3,7 @@ package com.twofasapp.features.backup
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.lifecycleScope
import com.twofasapp.base.BaseActivityPresenter
import com.twofasapp.data.session.SettingsRepository
import com.twofasapp.databinding.ActivityBackupBinding
@ -12,9 +13,11 @@ import com.twofasapp.extensions.navigationClicksThrottled
import com.twofasapp.features.backup.settings.BackupSettingsFragment
import com.twofasapp.features.backup.status.BackupStatusFragment
import com.twofasapp.resources.R
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class BackupActivity : BaseActivityPresenter<ActivityBackupBinding>(), BackupContract.View, BackupStatusFragment.Listener, BackupSettingsFragment.Listener {
class BackupActivity : BaseActivityPresenter<ActivityBackupBinding>(), BackupContract.View, BackupStatusFragment.Listener,
BackupSettingsFragment.Listener {
companion object {
const val EXTRA_IS_OPENED_FROM_BACKUP_NOTICE = "isOpenedFromBackupNotice"
@ -27,7 +30,13 @@ class BackupActivity : BaseActivityPresenter<ActivityBackupBinding>(), BackupCon
ThemeState.applyTheme(settingsRepository.getAppSettings().selectedTheme)
super.onCreate(savedInstanceState)
makeWindowSecure()
lifecycleScope.launch {
settingsRepository.observeAppSettings().collect {
makeWindowSecure(allow = it.allowScreenshots)
}
}
setContentView(ActivityBackupBinding::inflate)
setPresenter(presenter)

View File

@ -5,7 +5,9 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.text.InputType
import androidx.lifecycle.lifecycleScope
import com.twofasapp.base.BaseActivityPresenter
import com.twofasapp.data.session.SettingsRepository
import com.twofasapp.databinding.ActivityImportBackupBinding
import com.twofasapp.design.dialogs.InfoDialog
import com.twofasapp.design.dialogs.SimpleInputDialog
@ -15,6 +17,8 @@ import com.twofasapp.extensions.makeVisible
import com.twofasapp.extensions.makeWindowSecure
import com.twofasapp.extensions.navigationClicksThrottled
import com.twofasapp.extensions.toastLong
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class ImportBackupActivity : BaseActivityPresenter<ActivityImportBackupBinding>(), ImportBackupContract.View {
@ -23,10 +27,15 @@ class ImportBackupActivity : BaseActivityPresenter<ActivityImportBackupBinding>(
}
private val presenter: ImportBackupContract.Presenter by injectThis()
private val settingsRepository: SettingsRepository by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeWindowSecure()
lifecycleScope.launch {
settingsRepository.observeAppSettings().collect {
makeWindowSecure(allow = it.allowScreenshots)
}
}
setContentView(ActivityImportBackupBinding::inflate)
setPresenter(presenter)

View File

@ -49,7 +49,11 @@ class MainActivity : AppCompatActivity(), AuthAware {
override fun onCreate(savedInstanceState: Bundle?) {
ThemeState.applyTheme(settingsRepository.getAppSettings().selectedTheme)
super.onCreate(savedInstanceState)
makeWindowSecure()
lifecycleScope.launch {
settingsRepository.observeAppSettings().collect {
makeWindowSecure(allow = it.allowScreenshots)
}
}
setContent { MainScreen() }

View File

@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
import com.mikepenz.fastadapter.IItem
import com.mikepenz.fastadapter.adapters.FastItemAdapter
@ -21,6 +22,7 @@ import com.twofasapp.extensions.makeWindowSecure
import com.twofasapp.extensions.navigationClicksThrottled
import com.twofasapp.views.ModelDiffUtilCallback
import com.twofasapp.widgets.broadcast.WidgetBroadcaster
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class WidgetSettingsActivity : BaseActivityPresenter<ActivityWidgetSettingsBinding>(), WidgetSettingsContract.View, AuthAware {
@ -36,7 +38,11 @@ class WidgetSettingsActivity : BaseActivityPresenter<ActivityWidgetSettingsBindi
authTracker.onWidgetSettingsScreen()
super.onCreate(savedInstanceState)
makeWindowSecure()
lifecycleScope.launch {
settingsRepository.observeAppSettings().collect {
makeWindowSecure(allow = it.allowScreenshots)
}
}
setContentView(ActivityWidgetSettingsBinding::inflate)
setPresenter(presenter)
viewBinding.recycler.adapter = fastAdapter

View File

@ -5,32 +5,40 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.twofasapp.backup.databinding.ActivityExportBackupBinding
import com.twofasapp.base.BaseActivityPresenter
import com.twofasapp.common.environment.AppBuild
import com.twofasapp.core.RequestCodes
import com.twofasapp.data.session.SettingsRepository
import com.twofasapp.design.dialogs.InfoDialog
import com.twofasapp.extensions.clicksThrottled
import com.twofasapp.extensions.makeWindowSecure
import com.twofasapp.extensions.navigationClicksThrottled
import com.twofasapp.extensions.toastLong
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import java.io.File
import java.io.FileOutputStream
class ExportBackupActivity : BaseActivityPresenter<ActivityExportBackupBinding>(), ExportBackupContract.View, ExportBackupPasswordDialog.Listener {
class ExportBackupActivity : BaseActivityPresenter<ActivityExportBackupBinding>(), ExportBackupContract.View,
ExportBackupPasswordDialog.Listener {
companion object {
private const val EXPORT_FILE_PICKER = 48453
}
private val presenter: ExportBackupContract.Presenter by injectThis()
private val analyticsService: com.twofasapp.core.analytics.AnalyticsService by inject()
private val settingsRepository: SettingsRepository by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeWindowSecure()
lifecycleScope.launch {
settingsRepository.observeAppSettings().collect {
makeWindowSecure(allow = it.allowScreenshots)
}
}
setContentView(ActivityExportBackupBinding::inflate)
setPresenter(presenter)
}
@ -67,14 +75,14 @@ class ExportBackupActivity : BaseActivityPresenter<ActivityExportBackupBinding>(
try {
startActivityForResult(intent, EXPORT_FILE_PICKER)
} catch (e: ActivityNotFoundException) {
InfoDialog(
context = this,
title = "Error",
msg = "Could not find system file provider.\n\nIf you removed default documents application you need to restore it in order to make the export work."
).show()
} catch (e: Exception) {
toastLong("System error! Could not launch file provider!")
}
}

View File

@ -42,12 +42,12 @@ fun MainAppTheme(
WindowCompat.setDecorFitsSystemWindows(this, false)
if (BuildConfig.BUILD_TYPE.equals("release", true)) {
setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
// if (BuildConfig.BUILD_TYPE.equals("release", true)) {
// setFlags(
// WindowManager.LayoutParams.FLAG_SECURE,
// WindowManager.LayoutParams.FLAG_SECURE
// )
// }
}
}
}

View File

@ -60,4 +60,5 @@ object TwIcons {
val Close @Composable get() = painterResource(R.drawable.ic_close)
val ListStyle @Composable get() = painterResource(R.drawable.ic_list_style)
val IncrementHotp @Composable get() = painterResource(R.drawable.ic_increment_hotp)
val Screenshot @Composable get() = painterResource(R.drawable.ic_screenshot)
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M182.48,278.3L182.48,166.78q0,-44.31 30.85,-75.15 30.85,-30.85 75.15,-30.85L400,60.78v97.52L280,158.3v120h-97.52ZM288.48,899.22q-44.31,0 -75.15,-30.85 -30.85,-30.85 -30.85,-75.15L182.48,681.7L280,681.7v120h120v97.52L288.48,899.22ZM680,278.3v-120L560,158.3v-97.52h111.52q44.31,0 75.15,30.85 30.85,30.85 30.85,75.15v111.52L680,278.3ZM560,899.22v-97.52h120v-120h97.52v111.52q0,44.31 -30.85,75.15 -30.85,30.85 -75.15,30.85L560,899.22Z"/>
</vector>

View File

@ -148,14 +148,39 @@ class Strings(c: Context) {
val tokenRequestTitle = c.getString(R.string.browser__2fa_token_request_title)
val tokenRequestBody = c.getString(R.string.browser__2fa_token_request_content)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
// val browserExt = c.getString(R.string.)
val addTitle = c.getString(R.string.tokens__add_title)
val addDescription = c.getString(R.string.tokens__add_description)
val addOtherMethods = c.getString(R.string.tokens__add_other_methods)
val addEnterManual = c.getString(R.string.tokens__add_enter_manual)
val addFromGallery = c.getString(R.string.tokens__add_from_gallery)
val addSuccessTitle = c.getString(R.string.tokens__add_success_title)
val addSuccessDescription = c.getString(R.string.tokens__add_success_description)
val addManualTitle = c.getString(R.string.tokens__add_manual_title)
val addManualDescription = c.getString(R.string.tokens__add_manual_description)
val addManualServiceName = c.getString(R.string.tokens__add_manual_service_name)
val addManualServiceKey = c.getString(R.string.tokens__add_manual_service_key)
val addManualOther = c.getString(R.string.tokens__add_manual_other)
val addManualOtherOptional = c.getString(R.string.tokens__add_manual_other_optional)
val addManualDoneCta = c.getString(R.string.tokens__add_manual_done_cta)
val addManualHelpCta = c.getString(R.string.tokens__add_manual_help_cta)
val addManualAdvanced = c.getString(R.string.tokens__add_manual_advanced)
val addManualAdvancedDescription = c.getString(R.string.tokens__add_manual_advanced_description)
val addManualAdditionalInfo = c.getString(R.string.tokens__add_manual_additional_info)
val addManualAlgorithm = c.getString(R.string.tokens__algorithm)
val addManualRefreshTime = c.getString(R.string.tokens__refresh_time)
val addManualInitialCounter = c.getString(R.string.tokens__initial_counter)
val addManualDigits = c.getString(R.string.tokens__number_of_digits)
val addScanInvalidQrTitle = c.getString(R.string.tokens__qr_does_not_work)
val addScanInvalidQrBody = c.getString(R.string.tokens__qr_point_and_scan_again)
val addScanInvalidQrCta = c.getString(R.string.tokens__try_again)
val addScanServiceExistsTitle = c.getString(R.string.commons__warning)
val addScanServiceExistsBody = c.getString(R.string.tokens__service_already_exists)
val addScanServiceExistsPositiveCta = c.getString(R.string.commons__yes)
val addScanServiceExistsNegativeCta = c.getString(R.string.commons__no)
// val add = c.getString(R.string.tokens__add_)
}

View File

@ -16,4 +16,6 @@ dependencies {
implementation(libs.kotlinCoroutines)
implementation(libs.kotlinSerialization)
implementation(libs.workManager)
implementation(libs.timber)
}

View File

@ -16,4 +16,5 @@ interface SettingsRepository {
suspend fun setAutoFocusSearch(autoFocusSearch: Boolean)
suspend fun setShowBackupNotice(showBackupNotice: Boolean)
suspend fun setSendCrashLogs(sendCrashLogs: Boolean)
suspend fun setAllowScreenshots(allow: Boolean)
}

View File

@ -63,4 +63,10 @@ internal class SettingsRepositoryImpl(
local.setSendCrashLogs(sendCrashLogs)
}
}
override suspend fun setAllowScreenshots(allow: Boolean) {
withContext(dispatchers.io) {
local.setAllowScreenshots(allow)
}
}
}

View File

@ -6,6 +6,7 @@ import com.twofasapp.data.session.SettingsRepository
import com.twofasapp.data.session.SettingsRepositoryImpl
import com.twofasapp.data.session.local.SessionLocalSource
import com.twofasapp.data.session.local.SettingsLocalSource
import com.twofasapp.data.session.work.DisableScreenshotsWorkDispatcher
import com.twofasapp.di.KoinModule
import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.singleOf
@ -18,5 +19,7 @@ class DataSessionModule : KoinModule {
singleOf(::SettingsLocalSource)
singleOf(::SettingsRepositoryImpl) { bind<SettingsRepository>() }
singleOf(::DisableScreenshotsWorkDispatcher)
}
}

View File

@ -5,6 +5,7 @@ data class AppSettings(
val autoFocusSearch: Boolean = false,
val showBackupNotice: Boolean = false,
val sendCrashLogs: Boolean = false,
val allowScreenshots: Boolean = false,
val selectedTheme: SelectedTheme = SelectedTheme.Auto,
val servicesStyle: ServicesStyle = ServicesStyle.Default,
val servicesSort: ServicesSort = ServicesSort.Manual,

View File

@ -1,5 +1,7 @@
package com.twofasapp.data.session.local
import com.twofasapp.common.environment.AppBuild
import com.twofasapp.common.environment.BuildVariant
import com.twofasapp.data.session.domain.AppSettings
import com.twofasapp.data.session.domain.SelectedTheme
import com.twofasapp.data.session.domain.ServicesSort
@ -10,7 +12,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal class SettingsLocalSource(
private val preferences: PlainPreferences
private val preferences: PlainPreferences,
private val appBuild: AppBuild,
) {
companion object {
@ -21,7 +24,7 @@ internal class SettingsLocalSource(
private const val KeyServicesSort = "servicesSort"
private const val KeyAutoFocusSearch = "autoFocusSearch"
private const val KeySendCrashLogs = "sendCrashLogs"
private const val KeyAllowScreenshots = "allowScreenshots"
}
private val appSettingsFlow: MutableStateFlow<AppSettings> by lazy {
@ -38,6 +41,11 @@ internal class SettingsLocalSource(
autoFocusSearch = preferences.getBoolean(KeyAutoFocusSearch) ?: false,
showBackupNotice = preferences.getBoolean(KeyShowBackupNotice) ?: true,
sendCrashLogs = preferences.getBoolean(KeySendCrashLogs) ?: true,
allowScreenshots = preferences.getBoolean(KeyAllowScreenshots) ?: when (appBuild.buildVariant) {
BuildVariant.Release -> false
BuildVariant.ReleaseLocal -> true
BuildVariant.Debug -> true
},
selectedTheme = preferences.getString(KeySelectedTheme)?.let { SelectedTheme.valueOf(it) } ?: SelectedTheme.Auto,
servicesStyle = preferences.getString(KeyServicesStyle)?.let { ServicesStyle.valueOf(it) } ?: ServicesStyle.Default,
servicesSort = preferences.getString(KeyServicesSort)?.let { ServicesSort.valueOf(it) } ?: ServicesSort.Manual,
@ -78,4 +86,9 @@ internal class SettingsLocalSource(
appSettingsFlow.update { it.copy(sendCrashLogs = sendCrashLogs) }
preferences.putBoolean(KeySendCrashLogs, sendCrashLogs)
}
fun setAllowScreenshots(allow: Boolean) {
appSettingsFlow.update { it.copy(allowScreenshots = allow) }
preferences.putBoolean(KeyAllowScreenshots, allow)
}
}

View File

@ -0,0 +1,21 @@
package com.twofasapp.data.session.work
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.twofasapp.data.session.SettingsRepository
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class DisableScreenshotsWork(
appContext: Context,
workerParams: WorkerParameters,
) : CoroutineWorker(appContext, workerParams), KoinComponent {
private val settingsRepository: SettingsRepository by inject()
override suspend fun doWork(): Result {
settingsRepository.setAllowScreenshots(false)
return Result.success()
}
}

View File

@ -0,0 +1,22 @@
package com.twofasapp.data.session.work
import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.twofasapp.di.WorkDispatcher
import java.util.concurrent.TimeUnit
class DisableScreenshotsWorkDispatcher(
private val context: Context
) : WorkDispatcher {
fun dispatch() {
val request = OneTimeWorkRequestBuilder<DisableScreenshotsWork>()
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("DisableScreenshotsWork", ExistingWorkPolicy.REPLACE, request)
}
}

View File

@ -2,32 +2,22 @@ package com.twofasapp.extensions
import android.app.Activity
import android.content.res.Resources
import android.graphics.Rect
import android.graphics.RectF
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.twofasapp.extensions.R
import kotlin.math.roundToInt
fun AppCompatActivity.replaceFragment(layoutRes: Int, fragment: Fragment) {
supportFragmentManager.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.replace(layoutRes, fragment)
.commit()
}
fun Activity.makeWindowSecure() {
if (BuildConfig.BUILD_TYPE.equals("release", true)) {
window?.setFlags(
fun Activity.makeWindowSecure(allow: Boolean) {
if (allow) {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
}
fun doNothing() = Unit
fun String.splitIntoLetters(): String = replace(".".toRegex(), "$0 ")
@ -42,19 +32,6 @@ fun String.removeWhiteCharacters(): String {
return replace(" ", "")
}
val Activity.screenRectPx: Rect
get() = Resources.getSystem().displayMetrics.run { Rect(0, 0, widthPixels, heightPixels) }
val Activity.screenRectDp: RectF
get() = Resources.getSystem().displayMetrics.run {
RectF(
0f,
0f,
widthPixels.px2dp,
heightPixels.px2dp
)
}
val Number.px2dp: Float
get() = this.toFloat() / Resources.getSystem().displayMetrics.density

View File

@ -21,7 +21,7 @@ internal class PermissionActivity : FragmentActivity(), ActivityCompat.OnRequest
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeWindowSecure()
makeWindowSecure(allow = true)
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
val permission = intent.getStringExtra(PERMISSION_KEY)

View File

@ -5,12 +5,14 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.twofasapp.base.AuthTracker
import com.twofasapp.data.session.SettingsRepository
import com.twofasapp.design.theme.ThemeState
import com.twofasapp.designsystem.MainAppTheme
import com.twofasapp.extensions.makeWindowSecure
import com.twofasapp.resources.R
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class LockActivity : AppCompatActivity() {
@ -26,7 +28,11 @@ class LockActivity : AppCompatActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
super.onCreate(savedInstanceState)
makeWindowSecure()
lifecycleScope.launch {
settingsRepository.observeAppSettings().collect {
makeWindowSecure(allow = it.allowScreenshots)
}
}
setContent {
MainAppTheme {

View File

@ -24,8 +24,10 @@ import com.twofasapp.design.compose.HeaderEntry
import com.twofasapp.design.compose.SimpleEntry
import com.twofasapp.design.compose.SubtitleGravity
import com.twofasapp.design.compose.SwitchEntry
import com.twofasapp.designsystem.TwIcons
import com.twofasapp.designsystem.TwTheme
import com.twofasapp.designsystem.common.TwTopAppBar
import com.twofasapp.designsystem.dialog.ConfirmDialog
import com.twofasapp.designsystem.dialog.ListRadioDialog
import com.twofasapp.resources.R
import com.twofasapp.security.domain.model.LockMethod
@ -47,6 +49,7 @@ internal fun SecurityScreen(
var showTrailsDialog by remember { mutableStateOf(false) }
var showTimeoutDialog by remember { mutableStateOf(false) }
var showBiometricDialog by remember { mutableStateOf(false) }
var showScreenshotsConfirmDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
@ -70,6 +73,7 @@ internal fun SecurityScreen(
item {
SwitchEntry(
title = stringResource(id = R.string.settings__option_fingerprint),
subtitle = stringResource(id = R.string.settings__option_fingerprint_description),
icon = painterResource(id = R.drawable.ic_fingerprint_old),
isChecked = false,
isEnabled = false,
@ -77,11 +81,21 @@ internal fun SecurityScreen(
)
}
item { Divider(color = TwTheme.color.divider, modifier = Modifier.padding(vertical = 8.dp)) }
item {
Text(
text = stringResource(id = R.string.settings__option_fingerprint_description),
modifier = Modifier.padding(start = 24.dp, end = 16.dp, top = 8.dp),
style = MaterialTheme.typography.bodyMedium.copy(color = TwTheme.color.onSurfaceSecondary)
SwitchEntry(
title = stringResource(id = R.string.settings__option_screenshots),
subtitle = stringResource(id = R.string.settings__option_screenshots_description),
icon = TwIcons.Screenshot,
isChecked = uiState.allowScreenshots,
switch = { isChecked ->
if (isChecked) {
showScreenshotsConfirmDialog = true
} else {
viewModel.toggleScreenshots()
}
}
)
}
@ -168,6 +182,24 @@ internal fun SecurityScreen(
)
}
item { Divider(color = TwTheme.color.divider, modifier = Modifier.padding(vertical = 8.dp)) }
item {
SwitchEntry(
title = stringResource(id = R.string.settings__option_screenshots),
subtitle = stringResource(id = R.string.settings__option_screenshots_description),
icon = TwIcons.Screenshot,
isChecked = uiState.allowScreenshots,
switch = { isChecked ->
if (isChecked) {
showScreenshotsConfirmDialog = true
} else {
viewModel.toggleScreenshots()
}
}
)
}
item { Divider(color = TwTheme.color.divider) }
}
}
@ -210,5 +242,16 @@ internal fun SecurityScreen(
onDismiss = { showBiometricDialog = false },
).show()
}
if (showScreenshotsConfirmDialog) {
ConfirmDialog(
onDismissRequest = { showScreenshotsConfirmDialog = false },
title = stringResource(id = R.string.settings__option_screenshots_confirm_title),
body = stringResource(id = R.string.settings__option_screenshots_confirm_description),
negative = stringResource(id = R.string.commons__no),
positive = stringResource(id = R.string.commons__yes),
onConfirm = { viewModel.toggleScreenshots() }
)
}
}
}

View File

@ -10,4 +10,5 @@ internal data class SecurityUiState(
val pinTrials: PinTrials = PinTrials.Trials3,
val pinTimeout: PinTimeout = PinTimeout.Timeout5,
val pinDigits: PinDigits = PinDigits.Code4,
val allowScreenshots: Boolean = false,
)

View File

@ -3,6 +3,9 @@ package com.twofasapp.security.ui.security
import androidx.lifecycle.viewModelScope
import com.twofasapp.base.BaseViewModel
import com.twofasapp.base.dispatcher.Dispatchers
import com.twofasapp.common.ktx.launchScoped
import com.twofasapp.data.session.SettingsRepository
import com.twofasapp.data.session.work.DisableScreenshotsWorkDispatcher
import com.twofasapp.security.domain.EditLockMethodCase
import com.twofasapp.security.domain.EditPinOptionsCase
import com.twofasapp.security.domain.ObserveLockMethodCase
@ -12,7 +15,6 @@ import com.twofasapp.security.domain.model.PinOptions
import com.twofasapp.security.domain.model.PinTimeout
import com.twofasapp.security.domain.model.PinTrials
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -23,10 +25,11 @@ internal class SecurityViewModel(
private val observePinOptionsCase: ObservePinOptionsCase,
private val editPinOptionsCase: EditPinOptionsCase,
private val editLockMethodCase: EditLockMethodCase,
private val settingsRepository: SettingsRepository,
private val disableScreenshotsWorkDispatcher: DisableScreenshotsWorkDispatcher,
) : BaseViewModel() {
private val _uiState = MutableStateFlow(SecurityUiState())
val uiState = _uiState.asStateFlow()
val uiState = MutableStateFlow(SecurityUiState())
init {
viewModelScope.launch(dispatchers.io()) {
@ -35,7 +38,7 @@ internal class SecurityViewModel(
observePinOptionsCase.invoke()
) { a, b -> Pair(a, b) }
.collect { (lockMethod, pinOptions) ->
_uiState.update { state ->
uiState.update { state ->
state.copy(
lockMethod = lockMethod,
pinTrials = pinOptions.trials,
@ -45,6 +48,12 @@ internal class SecurityViewModel(
}
}
}
launchScoped {
settingsRepository.observeAppSettings().collect { appSettings ->
uiState.update { it.copy(allowScreenshots = appSettings.allowScreenshots) }
}
}
}
fun updatePinTrails(pinTrials: PinTrials) {
@ -62,6 +71,18 @@ internal class SecurityViewModel(
}
}
fun toggleScreenshots() {
val isAllowed = uiState.value.allowScreenshots.not()
launchScoped {
settingsRepository.setAllowScreenshots(isAllowed)
}
if (isAllowed) {
disableScreenshotsWorkDispatcher.dispatch()
}
}
private fun updatePinOptions(action: (PinOptions) -> PinOptions) {
viewModelScope.launch(dispatchers.io()) {
editPinOptionsCase(