[#748][#749] Preferences for sync

Adds an option to enable/disable background sync, as well as an option to keep the screen on during sync

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Carter Jernigan 2023-02-17 11:39:15 -05:00 committed by GitHub
parent e0d3bea3de
commit edaeda56da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 235 additions and 24 deletions

View File

@ -0,0 +1,26 @@
# Configure keep screen on setting - Because sync can take a long time, the app has a feature to keep the screen on until sync completes. The functionality to keep the screen on will apply to any screen while the app is open and syncing is actively in progress
1. Change the systemwide screen timeout in the Android Settings to something short, e.g. 15 seconds
1. Install the app
1. Launch the app
1. Get to the app's settings
1. Enable the "keep screen on while syncing" option
1. Assuming that sync will take a long time, leave the app open and do not touch the screen for more than the systemwide screen timeout duration
1. Verify that the screen does not turn off
1. Go to the settings and disable the "Keep screen on while syncing" option
1. Keeping the app on the screen, leave the device alone for more than the systemwide screen timeout
1. Verify that the screen does turn off
1. Wake the device
1. Turn the "Keep screen on while syncing" option back on
1. Return to the home screen
1. Leave the device alone until sync completes (this may take hours)
1. Verify when you return that the screen is off (the screen should turn off within the systemwide screen timeout after syncing completes)
# Disable background syncing
1. Install a debug build of the app and connect the device to a system with the Android developer tools installed
1. Perform a fresh install of the app
1. Go through the onboarding to get to the home screen
1. In the developer tools (App Inspection -> Background Task Inspector), verify that a periodic WorkManager job is scheduled or running
1. Go into the app's settings and disable the background sync option
1. In the developer tools, verify that no periodic WorkManager job is scheduled or running (e.g. it may be cancelled)
1. Go into the app's settings and re-enable the background sync option
1. In the developer tools, verify that a periodic WorkManager job is scheduled

View File

@ -45,6 +45,7 @@ class HomeTestSetup(
fun getDefaultContent() { fun getDefaultContent() {
Home( Home(
walletSnapshot, walletSnapshot,
isKeepScreenOnDuringSync = false,
emptyList(), emptyList(),
goScan = { goScan = {
onScanCount.incrementAndGet() onScanCount.incrementAndGet()

View File

@ -55,7 +55,7 @@ class SettingsViewTest : UiTestPrerequisites() {
fun rescan() = runTest { fun rescan() = runTest {
val testSetup = TestSetup(composeTestRule) val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getBackupCount()) assertEquals(0, testSetup.getRescanCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also { composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also {
it.performClick() it.performClick()
@ -64,12 +64,40 @@ class SettingsViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getRescanCount()) assertEquals(1, testSetup.getRescanCount())
} }
@Test
@MediumTest
fun toggle_background_sync() = runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getBackgroundSyncToggleCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_enable_background_sync)).also {
it.performClick()
}
assertEquals(1, testSetup.getBackgroundSyncToggleCount())
}
@Test
@MediumTest
fun toggle_keep_screen_on() = runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getKeepScreenOnSyncToggleCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_enable_keep_screen_on)).also {
it.performClick()
}
assertEquals(1, testSetup.getKeepScreenOnSyncToggleCount())
}
@Test @Test
@MediumTest @MediumTest
fun toggle_analytics() = runTest { fun toggle_analytics() = runTest {
val testSetup = TestSetup(composeTestRule) val testSetup = TestSetup(composeTestRule)
assertEquals(0, testSetup.getBackupCount()) assertEquals(0, testSetup.getAnalyticsToggleCount())
composeTestRule.onNodeWithText(getStringResource(R.string.settings_enable_analytics)).also { composeTestRule.onNodeWithText(getStringResource(R.string.settings_enable_analytics)).also {
it.performClick() it.performClick()
@ -83,6 +111,8 @@ class SettingsViewTest : UiTestPrerequisites() {
private val onBackCount = AtomicInteger(0) private val onBackCount = AtomicInteger(0)
private val onBackupCount = AtomicInteger(0) private val onBackupCount = AtomicInteger(0)
private val onRescanCount = AtomicInteger(0) private val onRescanCount = AtomicInteger(0)
private val onBackgroundSyncChangedCount = AtomicInteger(0)
private val onKeepScreenOnChangedCount = AtomicInteger(0)
private val onAnalyticsChangedCount = AtomicInteger(0) private val onAnalyticsChangedCount = AtomicInteger(0)
fun getOnBackCount(): Int { fun getOnBackCount(): Int {
@ -100,6 +130,16 @@ class SettingsViewTest : UiTestPrerequisites() {
return onRescanCount.get() return onRescanCount.get()
} }
fun getBackgroundSyncToggleCount(): Int {
composeTestRule.waitForIdle()
return onBackgroundSyncChangedCount.get()
}
fun getKeepScreenOnSyncToggleCount(): Int {
composeTestRule.waitForIdle()
return onKeepScreenOnChangedCount.get()
}
fun getAnalyticsToggleCount(): Int { fun getAnalyticsToggleCount(): Int {
composeTestRule.waitForIdle() composeTestRule.waitForIdle()
return onAnalyticsChangedCount.get() return onAnalyticsChangedCount.get()
@ -109,6 +149,8 @@ class SettingsViewTest : UiTestPrerequisites() {
composeTestRule.setContent { composeTestRule.setContent {
ZcashTheme { ZcashTheme {
Settings( Settings(
isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true, isAnalyticsEnabled = true,
onBack = { onBack = {
onBackCount.incrementAndGet() onBackCount.incrementAndGet()
@ -119,6 +161,12 @@ class SettingsViewTest : UiTestPrerequisites() {
onRescanWallet = { onRescanWallet = {
onRescanCount.incrementAndGet() onRescanCount.incrementAndGet()
}, },
onBackgroundSyncSettingsChanged = {
onBackgroundSyncChangedCount.incrementAndGet()
},
onIsKeepScreenOnDuringSyncSettingsChanged = {
onKeepScreenOnChangedCount.incrementAndGet()
},
onAnalyticsSettingsChanged = { onAnalyticsSettingsChanged = {
onAnalyticsChangedCount.incrementAndGet() onAnalyticsChangedCount.incrementAndGet()
} }

View File

@ -12,7 +12,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import co.electriccoin.zcash.ui.common.BindCompLocalProvider import co.electriccoin.zcash.ui.common.BindCompLocalProvider
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
@ -20,12 +23,18 @@ import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Override import co.electriccoin.zcash.ui.design.component.Override
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.backup.WrapBackup import co.electriccoin.zcash.ui.screen.backup.WrapBackup
import co.electriccoin.zcash.ui.screen.home.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding
import co.electriccoin.zcash.ui.screen.warning.WrapNotEnoughSpace import co.electriccoin.zcash.ui.screen.warning.WrapNotEnoughSpace
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.work.WorkIds
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -47,6 +56,8 @@ class MainActivity : ComponentActivity() {
setupSplashScreen() setupSplashScreen()
setupUiContent() setupUiContent()
monitorForBackgroundSync()
} }
private fun setupSplashScreen() { private fun setupSplashScreen() {
@ -116,6 +127,30 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun monitorForBackgroundSync() {
val isEnableBackgroundSyncFlow = run {
val homeViewModel by viewModels<HomeViewModel>()
val isSecretReadyFlow = walletViewModel.secretState.map { it is SecretState.Ready }
val isBackgroundSyncEnabledFlow = homeViewModel.isBackgroundSyncEnabled.filterNotNull()
isSecretReadyFlow.combine(isBackgroundSyncEnabledFlow) { isSecretReady, isBackgroundSyncEnabled ->
isSecretReady && isBackgroundSyncEnabled
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
isEnableBackgroundSyncFlow.collect { isEnableBackgroundSync ->
if (isEnableBackgroundSync) {
WorkIds.enableBackgroundSynchronization(application)
} else {
WorkIds.disableBackgroundSynchronization(application)
}
}
}
}
}
companion object { companion object {
@VisibleForTesting @VisibleForTesting
internal val SPLASH_SCREEN_DELAY = 0.seconds internal val SPLASH_SCREEN_DELAY = 0.seconds

View File

@ -13,6 +13,10 @@ object StandardPreferenceKeys {
// Default to true until https://github.com/zcash/secant-android-wallet/issues/304 // Default to true until https://github.com/zcash/secant-android-wallet/issues/304
val IS_ANALYTICS_ENABLED = BooleanPreferenceDefault(Key("is_analytics_enabled"), true) val IS_ANALYTICS_ENABLED = BooleanPreferenceDefault(Key("is_analytics_enabled"), true)
val IS_BACKGROUND_SYNC_ENABLED = BooleanPreferenceDefault(Key("is_background_sync_enabled"), true)
val IS_KEEP_SCREEN_ON_DURING_SYNC = BooleanPreferenceDefault(Key("is_keep_screen_on_during_sync"), true)
/** /**
* The fiat currency that the user prefers. * The fiat currency that the user prefers.
*/ */

View File

@ -14,6 +14,7 @@ import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.home.view.Home import co.electriccoin.zcash.ui.screen.home.view.Home
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.update.model.UpdateState
@ -53,9 +54,12 @@ internal fun WrapHome(
} }
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
if (null == walletSnapshot) { if (null == walletSnapshot) {
// Display loading indicator // Display loading indicator
} else { } else {
@ -71,6 +75,7 @@ internal fun WrapHome(
Home( Home(
walletSnapshot, walletSnapshot,
isKeepScreenOnDuringSync = isKeepScreenOnWhileSyncing,
transactionSnapshot, transactionSnapshot,
goScan = goScan, goScan = goScan,
goRequest = goRequest, goRequest = goRequest,

View File

@ -42,10 +42,12 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.crash.android.GlobalCrashReporter import co.electriccoin.zcash.crash.android.GlobalCrashReporter
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol
@ -67,6 +69,7 @@ fun ComposablePreview() {
GradientSurface { GradientSurface {
Home( Home(
WalletSnapshotFixture.new(), WalletSnapshotFixture.new(),
isKeepScreenOnDuringSync = false,
emptyList(), emptyList(),
goScan = {}, goScan = {},
goProfile = {}, goProfile = {},
@ -85,6 +88,7 @@ fun ComposablePreview() {
@Composable @Composable
fun Home( fun Home(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isKeepScreenOnDuringSync: Boolean?,
transactionHistory: List<CommonTransaction>, transactionHistory: List<CommonTransaction>,
goScan: () -> Unit, goScan: () -> Unit,
goProfile: () -> Unit, goProfile: () -> Unit,
@ -100,6 +104,7 @@ fun Home(
HomeMainContent( HomeMainContent(
paddingValues, paddingValues,
walletSnapshot, walletSnapshot,
isKeepScreenOnDuringSync = isKeepScreenOnDuringSync,
transactionHistory, transactionHistory,
goScan = goScan, goScan = goScan,
goProfile = goProfile, goProfile = goProfile,
@ -171,6 +176,7 @@ private fun DebugMenu(
private fun HomeMainContent( private fun HomeMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isKeepScreenOnDuringSync: Boolean?,
transactionHistory: List<CommonTransaction>, transactionHistory: List<CommonTransaction>,
goScan: () -> Unit, goScan: () -> Unit,
goProfile: () -> Unit, goProfile: () -> Unit,
@ -212,9 +218,20 @@ private fun HomeMainContent(
TertiaryButton(onClick = goRequest, text = stringResource(R.string.home_button_request)) TertiaryButton(onClick = goRequest, text = stringResource(R.string.home_button_request))
History(transactionHistory) History(transactionHistory)
if (isKeepScreenOnDuringSync == true && isSyncing(walletSnapshot.status)) {
DisableScreenTimeout()
}
} }
} }
private fun isSyncing(status: Synchronizer.Status): Boolean {
return status == Synchronizer.Status.DOWNLOADING ||
status == Synchronizer.Status.VALIDATING ||
status == Synchronizer.Status.SCANNING ||
status == Synchronizer.Status.ENHANCING
}
@Composable @Composable
@Suppress("LongMethod", "MagicNumber") @Suppress("LongMethod", "MagicNumber")
private fun Status( private fun Status(

View File

@ -0,0 +1,24 @@
package co.electriccoin.zcash.ui.screen.home.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
class HomeViewModel(application: Application) : AndroidViewModel(application) {
/**
* A flow of whether background sync is enabled
*/
val isBackgroundSyncEnabled: StateFlow<Boolean?> = flow {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
emitAll(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED.observe(preferenceProvider))
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT.inWholeMilliseconds), null)
}

View File

@ -32,7 +32,6 @@ import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
import co.electriccoin.zcash.work.WorkIds
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -151,7 +150,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
) )
// This is not the right API, because the transaction list could be very long and might need UI filtering // This is not the right API, because the transaction list could be very long and might need UI filtering
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val transactionSnapshot: StateFlow<List<CommonTransaction>> = synchronizer val transactionSnapshot: StateFlow<List<CommonTransaction>> = synchronizer
.flatMapLatest { .flatMapLatest {
if (null == it) { if (null == it) {
@ -205,8 +204,6 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
persistWalletMutex.withLock { persistWalletMutex.withLock {
EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(preferenceProvider, persistableWallet) EncryptedPreferenceKeys.PERSISTABLE_WALLET.putValue(preferenceProvider, persistableWallet)
} }
WorkIds.enableBackgroundSynchronization(application)
} }
} }

View File

@ -33,18 +33,29 @@ private fun WrapSettings(
val settingsViewModel by activity.viewModels<SettingsViewModel>() val settingsViewModel by activity.viewModels<SettingsViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val isBackgroundSyncEnabled = settingsViewModel.isBackgroundSync.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value
if (null == synchronizer || null == isAnalyticsEnabled) { @Suppress("ComplexCondition")
if (null == synchronizer || null == isAnalyticsEnabled || null == isBackgroundSyncEnabled || null == isKeepScreenOnWhileSyncing) {
// Display loading indicator // Display loading indicator
} else { } else {
Settings( Settings(
isAnalyticsEnabled, isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing,
isAnalyticsEnabled = isAnalyticsEnabled,
onBack = goBack, onBack = goBack,
onBackupWallet = goWalletBackup, onBackupWallet = goWalletBackup,
onRescanWallet = { onRescanWallet = {
walletViewModel.rescanBlockchain() walletViewModel.rescanBlockchain()
}, },
onBackgroundSyncSettingsChanged = {
settingsViewModel.setBackgroundSyncEnabled(it)
},
onIsKeepScreenOnDuringSyncSettingsChanged = {
settingsViewModel.setKeepScreenOnWhileSyncing(it)
},
onAnalyticsSettingsChanged = { onAnalyticsSettingsChanged = {
settingsViewModel.setAnalyticsEnabled(it) settingsViewModel.setAnalyticsEnabled(it)
} }

View File

@ -39,10 +39,14 @@ fun PreviewSettings() {
ZcashTheme(darkTheme = true) { ZcashTheme(darkTheme = true) {
GradientSurface { GradientSurface {
Settings( Settings(
isBackgroundSyncEnabled = true,
isKeepScreenOnDuringSyncEnabled = true,
isAnalyticsEnabled = true, isAnalyticsEnabled = true,
onBack = {}, onBack = {},
onBackupWallet = {}, onBackupWallet = {},
onRescanWallet = {}, onRescanWallet = {},
onBackgroundSyncSettingsChanged = {},
onIsKeepScreenOnDuringSyncSettingsChanged = {},
onAnalyticsSettingsChanged = {} onAnalyticsSettingsChanged = {}
) )
} }
@ -53,10 +57,14 @@ fun PreviewSettings() {
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun Settings( fun Settings(
isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean, isAnalyticsEnabled: Boolean,
onBack: () -> Unit, onBack: () -> Unit,
onBackupWallet: () -> Unit, onBackupWallet: () -> Unit,
onRescanWallet: () -> Unit, onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit onAnalyticsSettingsChanged: (Boolean) -> Unit
) { ) {
Scaffold(topBar = { Scaffold(topBar = {
@ -64,9 +72,13 @@ fun Settings(
}) { paddingValues -> }) { paddingValues ->
SettingsMainContent( SettingsMainContent(
paddingValues, paddingValues,
isAnalyticsEnabled, isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnDuringSyncEnabled,
isAnalyticsEnabled = isAnalyticsEnabled,
onBackupWallet = onBackupWallet, onBackupWallet = onBackupWallet,
onRescanWallet = onRescanWallet, onRescanWallet = onRescanWallet,
onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged,
onIsKeepScreenOnDuringSyncSettingsChanged = onIsKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged onAnalyticsSettingsChanged = onAnalyticsSettingsChanged
) )
} }
@ -94,9 +106,13 @@ private fun SettingsTopAppBar(onBack: () -> Unit) {
@Suppress("LongParameterList") @Suppress("LongParameterList")
private fun SettingsMainContent( private fun SettingsMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
isBackgroundSyncEnabled: Boolean,
isKeepScreenOnDuringSyncEnabled: Boolean,
isAnalyticsEnabled: Boolean, isAnalyticsEnabled: Boolean,
onBackupWallet: () -> Unit, onBackupWallet: () -> Unit,
onRescanWallet: () -> Unit, onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onIsKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit onAnalyticsSettingsChanged: (Boolean) -> Unit
) { ) {
Column( Column(
@ -104,9 +120,17 @@ private fun SettingsMainContent(
.padding(top = paddingValues.calculateTopPadding()) .padding(top = paddingValues.calculateTopPadding())
) { ) {
PrimaryButton(onClick = onBackupWallet, text = stringResource(id = R.string.settings_backup)) PrimaryButton(onClick = onBackupWallet, text = stringResource(id = R.string.settings_backup))
// We have decided to not include this in settings; see overflow debug menu instead
// DangerousButton(onClick = onWipeWallet, text = stringResource(id = R.string.settings_wipe))
TertiaryButton(onClick = onRescanWallet, text = stringResource(id = R.string.settings_rescan)) TertiaryButton(onClick = onRescanWallet, text = stringResource(id = R.string.settings_rescan))
SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_background_sync),
state = isBackgroundSyncEnabled,
onStateChange = { onBackgroundSyncSettingsChanged(!isBackgroundSyncEnabled) }
)
SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_keep_screen_on),
state = isKeepScreenOnDuringSyncEnabled,
onStateChange = { onIsKeepScreenOnDuringSyncSettingsChanged(!isKeepScreenOnDuringSyncEnabled) }
)
SwitchWithLabel( SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_analytics), label = stringResource(id = R.string.settings_enable_analytics),
state = isAnalyticsEnabled, state = isAnalyticsEnabled,

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.settings.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
@ -19,17 +20,34 @@ import kotlinx.coroutines.sync.withLock
class SettingsViewModel(application: Application) : AndroidViewModel(application) { class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val mutex = Mutex() private val mutex = Mutex()
val isAnalyticsEnabled: StateFlow<Boolean?> = flow<Boolean?> { val isAnalyticsEnabled: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_ANALYTICS_ENABLED)
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
emitAll(StandardPreferenceKeys.IS_ANALYTICS_ENABLED.observe(preferenceProvider)) val isBackgroundSync: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED)
val isKeepScreenOnWhileSyncing: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_KEEP_SCREEN_ON_DURING_SYNC)
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> = flow<Boolean?> {
val preferenceProvider = StandardPreferenceSingleton.getInstance(getApplication())
emitAll(default.observe(preferenceProvider))
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
fun setAnalyticsEnabled(enabled: Boolean) { fun setAnalyticsEnabled(enabled: Boolean) {
setBooleanPreference(StandardPreferenceKeys.IS_ANALYTICS_ENABLED, enabled)
}
fun setBackgroundSyncEnabled(enabled: Boolean) {
setBooleanPreference(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED, enabled)
}
fun setKeepScreenOnWhileSyncing(enabled: Boolean) {
setBooleanPreference(StandardPreferenceKeys.IS_KEEP_SCREEN_ON_DURING_SYNC, enabled)
}
private fun setBooleanPreference(default: BooleanPreferenceDefault, newState: Boolean) {
viewModelScope.launch { viewModelScope.launch {
val prefs = StandardPreferenceSingleton.getInstance(getApplication()) val prefs = StandardPreferenceSingleton.getInstance(getApplication())
mutex.withLock { mutex.withLock {
// Note that the Application object observes this and performs the actual side effects default.putValue(prefs, newState)
StandardPreferenceKeys.IS_ANALYTICS_ENABLED.putValue(prefs, enabled)
} }
} }
} }

View File

@ -7,13 +7,6 @@ import androidx.work.WorkManager
object WorkIds { object WorkIds {
const val WORK_ID_BACKGROUND_SYNC = "co.electriccoin.zcash.background_sync" const val WORK_ID_BACKGROUND_SYNC = "co.electriccoin.zcash.background_sync"
/*
* For now, sync is always enabled. In the future, we can consider whether a preference
* is a good idea.
*
* Also note that if we ever change the sync interval period, this code won't re-run on
* existing installations unless we make changes to call this during app startup.
*/
fun enableBackgroundSynchronization(context: Context) { fun enableBackgroundSynchronization(context: Context) {
val workManager = WorkManager.getInstance(context) val workManager = WorkManager.getInstance(context)
@ -23,4 +16,10 @@ object WorkIds {
SyncWorker.newWorkRequest() SyncWorker.newWorkRequest()
) )
} }
fun disableBackgroundSynchronization(context: Context) {
val workManager = WorkManager.getInstance(context)
workManager.cancelUniqueWork(WORK_ID_BACKGROUND_SYNC)
}
} }

View File

@ -6,6 +6,8 @@
<string name="settings_wipe">Wipe Wallet Data</string> <string name="settings_wipe">Wipe Wallet Data</string>
<string name="settings_rescan">Rescan Blockchain</string> <string name="settings_rescan">Rescan Blockchain</string>
<string name="settings_enable_background_sync">Background sync</string>
<string name="settings_enable_keep_screen_on">Keep screen on during sync</string>
<string name="settings_enable_analytics">Report crashes</string> <string name="settings_enable_analytics">Report crashes</string>
</resources> </resources>