mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-09 04:26:57 +00:00
Rewrite AccountSettingsActivity to M3 (#795)
* Extract composables * Drop sub component previews and minor adjustment * Fix preview * Extract view model * Switch to M3 * Extract URI to Constant * Minor changes * We alway have AccountSettings * Replace LiveData by State * Use Snapshot.withMutableSnapshot in reload * Don't show empty OAuth setting --------- Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
5cc29fc58a
commit
e2bfa8c56b
|
@ -26,6 +26,7 @@ object Constants {
|
|||
const val MANUAL_FRAGMENT_SERVICE_DISCOVERY = "how-does-service-discovery-work"
|
||||
const val MANUAL_PATH_SETTINGS = "settings.html"
|
||||
const val MANUAL_FRAGMENT_APP_SETTINGS = "app-wide-settings"
|
||||
const val MANUAL_FRAGMENT_ACCOUNT_SETTINGS = "account-settings"
|
||||
const val MANUAL_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
|
||||
|
||||
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()
|
||||
|
|
|
@ -5,152 +5,39 @@
|
|||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.security.KeyChain
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.SnackbarResult
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Help
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Contacts
|
||||
import androidx.compose.material.icons.filled.Event
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Password
|
||||
import androidx.compose.material.icons.filled.SyncProblem
|
||||
import androidx.compose.material.icons.filled.Wifi
|
||||
import androidx.compose.material.icons.outlined.Task
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.Syncer
|
||||
import at.bitfire.davdroid.ui.M2Theme
|
||||
import at.bitfire.davdroid.ui.composable.ActionCard
|
||||
import at.bitfire.davdroid.ui.composable.EditTextInputDialog
|
||||
import at.bitfire.davdroid.ui.composable.MultipleChoiceInputDialog
|
||||
import at.bitfire.davdroid.ui.composable.Setting
|
||||
import at.bitfire.davdroid.ui.composable.SettingsHeader
|
||||
import at.bitfire.davdroid.ui.composable.SwitchSetting
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.openid.appauth.AuthState
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AccountSettingsActivity: AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ACCOUNT = "account"
|
||||
|
||||
const val ACCOUNT_SETTINGS_HELP_URL = "https://manual.davx5.com/settings.html#account-settings"
|
||||
}
|
||||
|
||||
private val account by lazy {
|
||||
intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set")
|
||||
}
|
||||
|
||||
@Inject lateinit var modelFactory: Model.Factory
|
||||
val model by viewModels<Model> {
|
||||
object: ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>) =
|
||||
modelFactory.create(account) as T
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
title = account.name
|
||||
|
||||
setContent {
|
||||
M2Theme {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onSupportNavigateUp() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = stringResource(R.string.navigate_up))
|
||||
}
|
||||
},
|
||||
title = { Text(account.name) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
uriHandler.openUri(ACCOUNT_SETTINGS_HELP_URL)
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.help))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { padding ->
|
||||
Box(Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())) {
|
||||
AccountSettings_FromModel(
|
||||
snackbarHostState = snackbarHostState,
|
||||
model = model
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountSettingsScreen(
|
||||
account = account,
|
||||
onNavWifiPermissionsScreen = {
|
||||
val intent = Intent(this, WifiPermissionsActivity::class.java)
|
||||
intent.putExtra(WifiPermissionsActivity.EXTRA_ACCOUNT, account)
|
||||
startActivity(intent)
|
||||
},
|
||||
onNavUp = ::onSupportNavigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,675 +47,4 @@ class AccountSettingsActivity: AppCompatActivity() {
|
|||
builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun AccountSettings_FromModel(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
model: Model
|
||||
) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
SyncSettings(
|
||||
contactsSyncInterval = model.syncIntervalContacts.observeAsState().value,
|
||||
onUpdateContactsSyncInterval = { model.updateSyncInterval(getString(R.string.address_books_authority), it) },
|
||||
calendarSyncInterval = model.syncIntervalCalendars.observeAsState().value,
|
||||
onUpdateCalendarSyncInterval = { model.updateSyncInterval(CalendarContract.AUTHORITY, it) },
|
||||
taskSyncInterval = model.syncIntervalTasks.observeAsState().value,
|
||||
onUpdateTaskSyncInterval = { interval ->
|
||||
model.tasksProvider?.let { model.updateSyncInterval(it.authority, interval) }
|
||||
},
|
||||
syncOnlyOnWifi = model.syncWifiOnly.observeAsState(false).value,
|
||||
onUpdateSyncOnlyOnWifi = { model.updateSyncWifiOnly(it) },
|
||||
onlyOnSsids = model.syncWifiOnlySSIDs.observeAsState().value,
|
||||
onUpdateOnlyOnSsids = { model.updateSyncWifiOnlySSIDs(it) },
|
||||
ignoreVpns = model.ignoreVpns.observeAsState(false).value,
|
||||
onUpdateIgnoreVpns = { model.updateIgnoreVpns(it) }
|
||||
)
|
||||
|
||||
val credentials by model.credentials.observeAsState()
|
||||
credentials?.let {
|
||||
AuthenticationSettings(
|
||||
snackbarHostState = snackbarHostState,
|
||||
credentials = it,
|
||||
onUpdateCredentials = { model.updateCredentials(it) }
|
||||
)
|
||||
}
|
||||
|
||||
CalDavSettings(
|
||||
timeRangePastDays = model.timeRangePastDays.observeAsState().value,
|
||||
onUpdateTimeRangePastDays = { model.updateTimeRangePastDays(it) },
|
||||
defaultAlarmMinBefore = model.defaultAlarmMinBefore.observeAsState().value,
|
||||
onUpdateDefaultAlarmMinBefore = { model.updateDefaultAlarm(it) },
|
||||
manageCalendarColors = model.manageCalendarColors.observeAsState().value ?: false,
|
||||
onUpdateManageCalendarColors = { model.updateManageCalendarColors(it) },
|
||||
eventColors = model.eventColors.observeAsState().value ?: false,
|
||||
onUpdateEventColors = { model.updateEventColors(it) }
|
||||
)
|
||||
|
||||
CardDavSettings(
|
||||
contactGroupMethod = model.contactGroupMethod.observeAsState(GroupMethod.GROUP_VCARDS).value,
|
||||
onUpdateContactGroupMethod = { model.updateContactGroupMethod(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SyncSettings(
|
||||
contactsSyncInterval: Long?,
|
||||
onUpdateContactsSyncInterval: ((Long) -> Unit) = {},
|
||||
calendarSyncInterval: Long?,
|
||||
onUpdateCalendarSyncInterval: ((Long) -> Unit) = {},
|
||||
taskSyncInterval: Long?,
|
||||
onUpdateTaskSyncInterval: ((Long) -> Unit) = {},
|
||||
syncOnlyOnWifi: Boolean,
|
||||
onUpdateSyncOnlyOnWifi: (Boolean) -> Unit = {},
|
||||
onlyOnSsids: List<String>?,
|
||||
onUpdateOnlyOnSsids: (List<String>) -> Unit = {},
|
||||
ignoreVpns: Boolean,
|
||||
onUpdateIgnoreVpns: (Boolean) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column {
|
||||
SettingsHeader(false) {
|
||||
Text(stringResource(R.string.settings_sync))
|
||||
}
|
||||
|
||||
if (contactsSyncInterval != null)
|
||||
SyncIntervalSetting(
|
||||
icon = Icons.Default.Contacts,
|
||||
name = R.string.settings_sync_interval_contacts,
|
||||
syncInterval = contactsSyncInterval,
|
||||
onUpdateSyncInterval = onUpdateContactsSyncInterval
|
||||
)
|
||||
if (calendarSyncInterval != null)
|
||||
SyncIntervalSetting(
|
||||
icon = Icons.Default.Event,
|
||||
name = R.string.settings_sync_interval_calendars,
|
||||
syncInterval = calendarSyncInterval,
|
||||
onUpdateSyncInterval = onUpdateCalendarSyncInterval
|
||||
)
|
||||
if (taskSyncInterval != null)
|
||||
SyncIntervalSetting(
|
||||
icon = Icons.Outlined.Task,
|
||||
name = R.string.settings_sync_interval_tasks,
|
||||
syncInterval = taskSyncInterval,
|
||||
onUpdateSyncInterval = onUpdateTaskSyncInterval
|
||||
)
|
||||
|
||||
SwitchSetting(
|
||||
icon = Icons.Default.Wifi,
|
||||
name = stringResource(R.string.settings_sync_wifi_only),
|
||||
summaryOn = stringResource(R.string.settings_sync_wifi_only_on),
|
||||
summaryOff = stringResource(R.string.settings_sync_wifi_only_off),
|
||||
checked = syncOnlyOnWifi,
|
||||
onCheckedChange = onUpdateSyncOnlyOnWifi
|
||||
)
|
||||
|
||||
var showWifiOnlySsidsDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_sync_wifi_only_ssids),
|
||||
enabled = syncOnlyOnWifi,
|
||||
summary =
|
||||
if (onlyOnSsids != null)
|
||||
stringResource(R.string.settings_sync_wifi_only_ssids_on, onlyOnSsids.joinToString(", "))
|
||||
else
|
||||
stringResource(R.string.settings_sync_wifi_only_ssids_off),
|
||||
onClick = {
|
||||
showWifiOnlySsidsDialog = true
|
||||
}
|
||||
)
|
||||
if (showWifiOnlySsidsDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_sync_wifi_only_ssids_message),
|
||||
initialValue = onlyOnSsids?.joinToString(", ") ?: "",
|
||||
onValueEntered = { newValue ->
|
||||
val newSsids = newValue.split(',')
|
||||
.map { it.trim() }
|
||||
.distinct()
|
||||
onUpdateOnlyOnSsids(newSsids)
|
||||
showWifiOnlySsidsDialog = false
|
||||
},
|
||||
onDismiss = { showWifiOnlySsidsDialog = false }
|
||||
)
|
||||
|
||||
val canAccessWifiSsid by PermissionUtils.rememberCanAccessWifiSsid()
|
||||
if (LocalInspectionMode.current || (onlyOnSsids != null && !canAccessWifiSsid))
|
||||
ActionCard(
|
||||
icon = Icons.Default.SyncProblem,
|
||||
actionText = stringResource(R.string.settings_sync_wifi_only_ssids_permissions_action),
|
||||
onAction = {
|
||||
val intent = Intent(context, WifiPermissionsActivity::class.java)
|
||||
intent.putExtra(WifiPermissionsActivity.EXTRA_ACCOUNT, account)
|
||||
startActivity(intent)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.settings_sync_wifi_only_ssids_permissions_required))
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_ignore_vpns),
|
||||
summaryOn = stringResource(R.string.settings_ignore_vpns_on),
|
||||
summaryOff = stringResource(R.string.settings_ignore_vpns_off),
|
||||
checked = ignoreVpns,
|
||||
onCheckedChange = onUpdateIgnoreVpns
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SyncIntervalSetting(
|
||||
icon: ImageVector,
|
||||
@StringRes name: Int,
|
||||
syncInterval: Long,
|
||||
onUpdateSyncInterval: (Long) -> Unit
|
||||
) {
|
||||
var showSyncIntervalDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = icon,
|
||||
name = stringResource(name),
|
||||
summary =
|
||||
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
stringResource(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
stringResource(R.string.settings_sync_summary_periodically, syncInterval / 60),
|
||||
onClick = {
|
||||
showSyncIntervalDialog = true
|
||||
}
|
||||
)
|
||||
if (showSyncIntervalDialog) {
|
||||
val syncIntervalNames = stringArrayResource(R.array.settings_sync_interval_names)
|
||||
val syncIntervalSeconds = stringArrayResource(R.array.settings_sync_interval_seconds)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(name),
|
||||
namesAndValues = syncIntervalNames.zip(syncIntervalSeconds),
|
||||
initialValue = syncInterval.toString(),
|
||||
onValueSelected = { newValue ->
|
||||
try {
|
||||
val seconds = newValue.toLong()
|
||||
onUpdateSyncInterval(seconds)
|
||||
} catch (_: NumberFormatException) {
|
||||
}
|
||||
showSyncIntervalDialog = false
|
||||
},
|
||||
onDismiss = {
|
||||
showSyncIntervalDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SyncSettings_Preview() {
|
||||
SyncSettings(
|
||||
contactsSyncInterval = 60*60,
|
||||
calendarSyncInterval = 4*60*60,
|
||||
taskSyncInterval = 2*60*60,
|
||||
syncOnlyOnWifi = false,
|
||||
onlyOnSsids = listOf("SSID1", "SSID2"),
|
||||
ignoreVpns = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthenticationSettings(
|
||||
credentials: Credentials,
|
||||
snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
onUpdateCredentials: (Credentials) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
SettingsHeader(false) {
|
||||
Text(stringResource(R.string.settings_authentication))
|
||||
}
|
||||
|
||||
if (credentials.authState != null) { // OAuth
|
||||
Setting(
|
||||
icon = Icons.Default.Password,
|
||||
name = stringResource(R.string.settings_oauth),
|
||||
summary = stringResource(R.string.settings_oauth_summary),
|
||||
onClick = {
|
||||
// GoogleLoginFragment replacement
|
||||
}
|
||||
)
|
||||
|
||||
} else { // username/password
|
||||
if (credentials.username != null) {
|
||||
var showUsernameDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.AccountCircle,
|
||||
name = stringResource(R.string.settings_username),
|
||||
summary = credentials.username,
|
||||
onClick = {
|
||||
showUsernameDialog = true
|
||||
}
|
||||
)
|
||||
if (showUsernameDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_username),
|
||||
initialValue = credentials.username,
|
||||
onValueEntered = { newValue ->
|
||||
onUpdateCredentials(credentials.copy(username = newValue))
|
||||
},
|
||||
onDismiss = { showUsernameDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (credentials.password != null) {
|
||||
var showPasswordDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.Password,
|
||||
name = stringResource(R.string.settings_password),
|
||||
summary = stringResource(R.string.settings_password_summary),
|
||||
onClick = {
|
||||
showPasswordDialog = true
|
||||
}
|
||||
)
|
||||
if (showPasswordDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_password),
|
||||
inputLabel = stringResource(R.string.settings_new_password),
|
||||
initialValue = null, // Do not show the existing password
|
||||
passwordField = true,
|
||||
onValueEntered = { newValue ->
|
||||
onUpdateCredentials(credentials.copy(password = newValue))
|
||||
},
|
||||
onDismiss = { showPasswordDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
// client certificate
|
||||
Setting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_certificate_alias),
|
||||
summary = credentials.certificateAlias ?: stringResource(R.string.settings_certificate_alias_empty),
|
||||
onClick = {
|
||||
val activity = context as Activity
|
||||
KeyChain.choosePrivateKeyAlias(activity, { newAlias ->
|
||||
if (newAlias != null)
|
||||
onUpdateCredentials(credentials.copy(certificateAlias = newAlias))
|
||||
else
|
||||
scope.launch {
|
||||
if (snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.settings_certificate_alias_empty),
|
||||
actionLabel = context.getString(R.string.settings_certificate_install).uppercase()
|
||||
) == SnackbarResult.ActionPerformed) {
|
||||
val intent = KeyChain.createInstallIntent()
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}, null, null, null, -1, credentials.certificateAlias)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AuthenticationSettings_Preview_ClientCertificate() {
|
||||
AuthenticationSettings(
|
||||
credentials = Credentials(certificateAlias = "alias")
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AuthenticationSettings_Preview_OAuth() {
|
||||
AuthenticationSettings(
|
||||
credentials = Credentials(authState = AuthState())
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AuthenticationSettings_Preview_UsernamePassword() {
|
||||
AuthenticationSettings(
|
||||
credentials = Credentials(username = "user", password = "password")
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AuthenticationSettings_Preview_UsernamePassword_ClientCertificate() {
|
||||
AuthenticationSettings(
|
||||
credentials = Credentials(username = "user", password = "password", certificateAlias = "alias")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun CalDavSettings(
|
||||
timeRangePastDays: Int?,
|
||||
onUpdateTimeRangePastDays: (Int?) -> Unit = {},
|
||||
defaultAlarmMinBefore: Int?,
|
||||
onUpdateDefaultAlarmMinBefore: (Int?) -> Unit = {},
|
||||
manageCalendarColors: Boolean,
|
||||
onUpdateManageCalendarColors: (Boolean) -> Unit = {},
|
||||
eventColors: Boolean,
|
||||
onUpdateEventColors: (Boolean) -> Unit = {}
|
||||
) {
|
||||
Column {
|
||||
SettingsHeader {
|
||||
Text(stringResource(R.string.settings_caldav))
|
||||
}
|
||||
|
||||
var showTimeRangePastDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.History,
|
||||
name = stringResource(R.string.settings_sync_time_range_past),
|
||||
summary =
|
||||
if (timeRangePastDays != null)
|
||||
pluralStringResource(R.plurals.settings_sync_time_range_past_days, timeRangePastDays, timeRangePastDays)
|
||||
else
|
||||
stringResource(R.string.settings_sync_time_range_past_none),
|
||||
onClick = {
|
||||
showTimeRangePastDialog = true
|
||||
}
|
||||
)
|
||||
if (showTimeRangePastDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_sync_time_range_past_message),
|
||||
initialValue = timeRangePastDays?.toString() ?: "",
|
||||
onValueEntered = { newValue ->
|
||||
val days = try {
|
||||
newValue.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
onUpdateTimeRangePastDays(days)
|
||||
showTimeRangePastDialog = false
|
||||
},
|
||||
onDismiss = { showTimeRangePastDialog = false }
|
||||
)
|
||||
|
||||
var showDefaultAlarmDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_default_alarm),
|
||||
summary =
|
||||
if (defaultAlarmMinBefore != null)
|
||||
pluralStringResource(R.plurals.settings_default_alarm_on, defaultAlarmMinBefore, defaultAlarmMinBefore)
|
||||
else
|
||||
stringResource(R.string.settings_default_alarm_off),
|
||||
onClick = {
|
||||
showDefaultAlarmDialog = true
|
||||
}
|
||||
)
|
||||
if (showDefaultAlarmDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_default_alarm_message),
|
||||
initialValue = defaultAlarmMinBefore?.toString() ?: "",
|
||||
onValueEntered = { newValue ->
|
||||
val minBefore = try {
|
||||
newValue.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
onUpdateDefaultAlarmMinBefore(minBefore)
|
||||
showDefaultAlarmDialog = false
|
||||
},
|
||||
onDismiss = { showDefaultAlarmDialog = false }
|
||||
)
|
||||
|
||||
SwitchSetting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_manage_calendar_colors),
|
||||
summaryOn = stringResource(R.string.settings_manage_calendar_colors_on),
|
||||
summaryOff = stringResource(R.string.settings_manage_calendar_colors_off),
|
||||
checked = manageCalendarColors,
|
||||
onCheckedChange = onUpdateManageCalendarColors
|
||||
)
|
||||
|
||||
SwitchSetting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_event_colors),
|
||||
summaryOn = stringResource(R.string.settings_event_colors_on),
|
||||
summaryOff = stringResource(R.string.settings_event_colors_off),
|
||||
checked = eventColors,
|
||||
onCheckedChange = onUpdateEventColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun CalDavSettings_Preview() {
|
||||
CalDavSettings(
|
||||
timeRangePastDays = 30,
|
||||
defaultAlarmMinBefore = 10,
|
||||
manageCalendarColors = true,
|
||||
eventColors = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CardDavSettings(
|
||||
contactGroupMethod: GroupMethod,
|
||||
onUpdateContactGroupMethod: (GroupMethod) -> Unit = {}
|
||||
) {
|
||||
Column {
|
||||
SettingsHeader {
|
||||
Text(stringResource(R.string.settings_carddav))
|
||||
}
|
||||
|
||||
val groupMethodNames = stringArrayResource(R.array.settings_contact_group_method_entries)
|
||||
val groupMethodValues = stringArrayResource(R.array.settings_contact_group_method_values)
|
||||
var showGroupMethodDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.Contacts,
|
||||
name = stringResource(R.string.settings_contact_group_method),
|
||||
summary = groupMethodNames[groupMethodValues.indexOf(contactGroupMethod.name)],
|
||||
onClick = {
|
||||
showGroupMethodDialog = true
|
||||
}
|
||||
)
|
||||
if (showGroupMethodDialog)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(R.string.settings_contact_group_method),
|
||||
namesAndValues = groupMethodNames.zip(groupMethodValues),
|
||||
initialValue = contactGroupMethod.name,
|
||||
onValueSelected = { newValue ->
|
||||
onUpdateContactGroupMethod(GroupMethod.valueOf(newValue))
|
||||
showGroupMethodDialog = false
|
||||
},
|
||||
onDismiss = { showGroupMethodDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun CardDavSettings_Preview() {
|
||||
CardDavSettings(
|
||||
contactGroupMethod = GroupMethod.GROUP_VCARDS
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class Model @AssistedInject constructor(
|
||||
val context: Application,
|
||||
val settings: SettingsManager,
|
||||
@Assisted val account: Account
|
||||
): ViewModel(), SettingsManager.OnChangeListener {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(account: Account): Model
|
||||
}
|
||||
|
||||
private var accountSettings: AccountSettings? = null
|
||||
|
||||
// settings
|
||||
val syncIntervalContacts = MutableLiveData<Long>()
|
||||
val syncIntervalCalendars = MutableLiveData<Long>()
|
||||
|
||||
val tasksProvider = TaskUtils.currentProvider(context)
|
||||
val syncIntervalTasks = MutableLiveData<Long>()
|
||||
|
||||
val syncWifiOnly = MutableLiveData<Boolean>()
|
||||
val syncWifiOnlySSIDs = MutableLiveData<List<String>>()
|
||||
val ignoreVpns = MutableLiveData<Boolean>()
|
||||
|
||||
val credentials = MutableLiveData<Credentials>()
|
||||
|
||||
val timeRangePastDays = MutableLiveData<Int>()
|
||||
val defaultAlarmMinBefore = MutableLiveData<Int>()
|
||||
val manageCalendarColors = MutableLiveData<Boolean>()
|
||||
val eventColors = MutableLiveData<Boolean>()
|
||||
|
||||
val contactGroupMethod = MutableLiveData<GroupMethod>()
|
||||
|
||||
|
||||
init {
|
||||
accountSettings = AccountSettings(context, account)
|
||||
|
||||
settings.addOnChangeListener(this)
|
||||
|
||||
reload()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
settings.removeOnChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onSettingsChanged() {
|
||||
Logger.log.info("Settings changed")
|
||||
reload()
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
val accountSettings = accountSettings ?: return
|
||||
|
||||
syncIntervalContacts.postValue(
|
||||
accountSettings.getSyncInterval(context.getString(R.string.address_books_authority))
|
||||
)
|
||||
syncIntervalCalendars.postValue(accountSettings.getSyncInterval(CalendarContract.AUTHORITY))
|
||||
syncIntervalTasks.postValue(tasksProvider?.let { accountSettings.getSyncInterval(it.authority) })
|
||||
|
||||
syncWifiOnly.postValue(accountSettings.getSyncWifiOnly())
|
||||
syncWifiOnlySSIDs.postValue(accountSettings.getSyncWifiOnlySSIDs())
|
||||
ignoreVpns.postValue(accountSettings.getIgnoreVpns())
|
||||
|
||||
credentials.postValue(accountSettings.credentials())
|
||||
|
||||
timeRangePastDays.postValue(accountSettings.getTimeRangePastDays())
|
||||
defaultAlarmMinBefore.postValue(accountSettings.getDefaultAlarm())
|
||||
manageCalendarColors.postValue(accountSettings.getManageCalendarColors())
|
||||
eventColors.postValue(accountSettings.getEventColors())
|
||||
|
||||
contactGroupMethod.postValue(accountSettings.getGroupMethod())
|
||||
}
|
||||
|
||||
|
||||
fun updateSyncInterval(authority: String, syncInterval: Long) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
accountSettings?.setSyncInterval(authority, syncInterval)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSyncWifiOnly(wifiOnly: Boolean) {
|
||||
accountSettings?.setSyncWiFiOnly(wifiOnly)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateSyncWifiOnlySSIDs(ssids: List<String>?) {
|
||||
accountSettings?.setSyncWifiOnlySSIDs(ssids)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateIgnoreVpns(ignoreVpns: Boolean) {
|
||||
accountSettings?.setIgnoreVpns(ignoreVpns)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateCredentials(credentials: Credentials) {
|
||||
accountSettings?.credentials(credentials)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateTimeRangePastDays(days: Int?) {
|
||||
accountSettings?.setTimeRangePastDays(days)
|
||||
reload()
|
||||
|
||||
/* If the new setting is a certain number of days, no full resync is required,
|
||||
because every sync will cause a REPORT calendar-query with the given number of days.
|
||||
However, if the new setting is "all events", collection sync may/should be used, so
|
||||
the last sync-token has to be reset, which is done by setting fullResync=true.
|
||||
*/
|
||||
resyncCalendars(fullResync = days == null, tasks = false)
|
||||
}
|
||||
|
||||
fun updateDefaultAlarm(minBefore: Int?) {
|
||||
accountSettings?.setDefaultAlarm(minBefore)
|
||||
reload()
|
||||
|
||||
resyncCalendars(fullResync = true, tasks = false)
|
||||
}
|
||||
|
||||
fun updateManageCalendarColors(manage: Boolean) {
|
||||
accountSettings?.setManageCalendarColors(manage)
|
||||
reload()
|
||||
|
||||
resyncCalendars(fullResync = false, tasks = true)
|
||||
}
|
||||
|
||||
fun updateEventColors(manageColors: Boolean) {
|
||||
accountSettings?.setEventColors(manageColors)
|
||||
reload()
|
||||
|
||||
resyncCalendars(fullResync = true, tasks = false)
|
||||
}
|
||||
|
||||
fun updateContactGroupMethod(groupMethod: GroupMethod) {
|
||||
accountSettings?.setGroupMethod(groupMethod)
|
||||
reload()
|
||||
|
||||
resync(
|
||||
authority = context.getString(R.string.address_books_authority),
|
||||
fullResync = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates calendar re-synchronization.
|
||||
*
|
||||
* @param fullResync whether sync shall download all events again
|
||||
* (_true_: sets [Syncer.SYNC_EXTRAS_FULL_RESYNC],
|
||||
* _false_: sets [Syncer.SYNC_EXTRAS_RESYNC])
|
||||
* @param tasks whether tasks shall be synchronized, too (false: only events, true: events and tasks)
|
||||
*/
|
||||
private fun resyncCalendars(fullResync: Boolean, tasks: Boolean) {
|
||||
resync(CalendarContract.AUTHORITY, fullResync)
|
||||
if (tasks)
|
||||
resync(TaskProvider.ProviderName.OpenTasks.authority, fullResync)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates re-synchronization for given authority.
|
||||
*
|
||||
* @param authority authority to re-sync
|
||||
* @param fullResync whether sync shall download all events again
|
||||
* (_true_: sets [Syncer.SYNC_EXTRAS_FULL_RESYNC],
|
||||
* _false_: sets [Syncer.SYNC_EXTRAS_RESYNC])
|
||||
*/
|
||||
private fun resync(authority: String, fullResync: Boolean) {
|
||||
val resync =
|
||||
if (fullResync)
|
||||
OneTimeSyncWorker.FULL_RESYNC
|
||||
else
|
||||
OneTimeSyncWorker.RESYNC
|
||||
OneTimeSyncWorker.enqueue(context, account, authority, resync = resync)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Application
|
||||
import android.provider.CalendarContract
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.Syncer
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel(assistedFactory = AccountSettingsModel.Factory::class)
|
||||
class AccountSettingsModel @AssistedInject constructor(
|
||||
val context: Application,
|
||||
val settings: SettingsManager,
|
||||
@Assisted val account: Account
|
||||
): ViewModel(), SettingsManager.OnChangeListener {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(account: Account): AccountSettingsModel
|
||||
}
|
||||
|
||||
private val accountSettings = AccountSettings(context, account)
|
||||
|
||||
// settings
|
||||
var syncIntervalContacts by mutableStateOf<Long?>(null)
|
||||
var syncIntervalCalendars by mutableStateOf<Long?>(null)
|
||||
|
||||
private val tasksProvider = TaskUtils.currentProvider(context)
|
||||
var syncIntervalTasks by mutableStateOf<Long?>(null)
|
||||
|
||||
var syncWifiOnly by mutableStateOf(false)
|
||||
var syncWifiOnlySSIDs by mutableStateOf<List<String>?>(null)
|
||||
var ignoreVpns by mutableStateOf(false)
|
||||
|
||||
var credentials by mutableStateOf(Credentials())
|
||||
|
||||
var timeRangePastDays by mutableStateOf<Int?>(null)
|
||||
var defaultAlarmMinBefore by mutableStateOf<Int?>(null)
|
||||
var manageCalendarColors by mutableStateOf(false)
|
||||
var eventColors by mutableStateOf(false)
|
||||
|
||||
var contactGroupMethod by mutableStateOf(GroupMethod.GROUP_VCARDS)
|
||||
|
||||
|
||||
init {
|
||||
settings.addOnChangeListener(this)
|
||||
reload()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
settings.removeOnChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onSettingsChanged() {
|
||||
reload()
|
||||
}
|
||||
|
||||
private fun reload() {
|
||||
Logger.log.info("Reloading settings")
|
||||
|
||||
Snapshot.withMutableSnapshot {
|
||||
syncIntervalContacts = accountSettings.getSyncInterval(context.getString(R.string.address_books_authority))
|
||||
syncIntervalCalendars = accountSettings.getSyncInterval(CalendarContract.AUTHORITY)
|
||||
syncIntervalTasks = tasksProvider?.let { accountSettings.getSyncInterval(it.authority) }
|
||||
|
||||
syncWifiOnly = accountSettings.getSyncWifiOnly()
|
||||
syncWifiOnlySSIDs = accountSettings.getSyncWifiOnlySSIDs()
|
||||
ignoreVpns = accountSettings.getIgnoreVpns()
|
||||
|
||||
credentials = accountSettings.credentials()
|
||||
|
||||
timeRangePastDays = accountSettings.getTimeRangePastDays()
|
||||
defaultAlarmMinBefore = accountSettings.getDefaultAlarm()
|
||||
manageCalendarColors = accountSettings.getManageCalendarColors()
|
||||
eventColors = accountSettings.getEventColors()
|
||||
|
||||
contactGroupMethod = accountSettings.getGroupMethod()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateContactsSyncInterval(syncInterval: Long) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
accountSettings.setSyncInterval(context.getString(R.string.address_books_authority), syncInterval)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCalendarSyncInterval(syncInterval: Long) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
accountSettings.setSyncInterval(CalendarContract.AUTHORITY, syncInterval)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTasksSyncInterval(syncInterval: Long) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
tasksProvider?.authority?.let { tasksAuthority ->
|
||||
accountSettings.setSyncInterval(tasksAuthority, syncInterval)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSyncWifiOnly(wifiOnly: Boolean) {
|
||||
accountSettings.setSyncWiFiOnly(wifiOnly)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateSyncWifiOnlySSIDs(ssids: List<String>?) {
|
||||
accountSettings.setSyncWifiOnlySSIDs(ssids)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateIgnoreVpns(ignoreVpns: Boolean) {
|
||||
accountSettings.setIgnoreVpns(ignoreVpns)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateCredentials(credentials: Credentials) {
|
||||
accountSettings.credentials(credentials)
|
||||
reload()
|
||||
}
|
||||
|
||||
fun updateTimeRangePastDays(days: Int?) {
|
||||
accountSettings.setTimeRangePastDays(days)
|
||||
reload()
|
||||
|
||||
/* If the new setting is a certain number of days, no full resync is required,
|
||||
because every sync will cause a REPORT calendar-query with the given number of days.
|
||||
However, if the new setting is "all events", collection sync may/should be used, so
|
||||
the last sync-token has to be reset, which is done by setting fullResync=true.
|
||||
*/
|
||||
resyncCalendars(fullResync = days == null, tasks = false)
|
||||
}
|
||||
|
||||
fun updateDefaultAlarm(minBefore: Int?) {
|
||||
accountSettings.setDefaultAlarm(minBefore)
|
||||
reload()
|
||||
|
||||
resyncCalendars(fullResync = true, tasks = false)
|
||||
}
|
||||
|
||||
fun updateManageCalendarColors(manage: Boolean) {
|
||||
accountSettings.setManageCalendarColors(manage)
|
||||
reload()
|
||||
|
||||
resyncCalendars(fullResync = false, tasks = true)
|
||||
}
|
||||
|
||||
fun updateEventColors(manageColors: Boolean) {
|
||||
accountSettings.setEventColors(manageColors)
|
||||
reload()
|
||||
|
||||
resyncCalendars(fullResync = true, tasks = false)
|
||||
}
|
||||
|
||||
fun updateContactGroupMethod(groupMethod: GroupMethod) {
|
||||
accountSettings.setGroupMethod(groupMethod)
|
||||
reload()
|
||||
|
||||
resync(
|
||||
authority = context.getString(R.string.address_books_authority),
|
||||
fullResync = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates calendar re-synchronization.
|
||||
*
|
||||
* @param fullResync whether sync shall download all events again
|
||||
* (_true_: sets [Syncer.SYNC_EXTRAS_FULL_RESYNC],
|
||||
* _false_: sets [Syncer.SYNC_EXTRAS_RESYNC])
|
||||
* @param tasks whether tasks shall be synchronized, too (false: only events, true: events and tasks)
|
||||
*/
|
||||
private fun resyncCalendars(fullResync: Boolean, tasks: Boolean) {
|
||||
resync(CalendarContract.AUTHORITY, fullResync)
|
||||
if (tasks)
|
||||
resync(TaskProvider.ProviderName.OpenTasks.authority, fullResync)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates re-synchronization for given authority.
|
||||
*
|
||||
* @param authority authority to re-sync
|
||||
* @param fullResync whether sync shall download all events again
|
||||
* (_true_: sets [Syncer.SYNC_EXTRAS_FULL_RESYNC],
|
||||
* _false_: sets [Syncer.SYNC_EXTRAS_RESYNC])
|
||||
*/
|
||||
private fun resync(authority: String, fullResync: Boolean) {
|
||||
val resync =
|
||||
if (fullResync)
|
||||
OneTimeSyncWorker.FULL_RESYNC
|
||||
else
|
||||
OneTimeSyncWorker.RESYNC
|
||||
OneTimeSyncWorker.enqueue(context, account, authority, resync = resync)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,714 @@
|
|||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Help
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Contacts
|
||||
import androidx.compose.material.icons.filled.Event
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Password
|
||||
import androidx.compose.material.icons.filled.SyncProblem
|
||||
import androidx.compose.material.icons.filled.Wifi
|
||||
import androidx.compose.material.icons.outlined.Task
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.AppTheme
|
||||
import at.bitfire.davdroid.ui.composable.ActionCard
|
||||
import at.bitfire.davdroid.ui.composable.EditTextInputDialog
|
||||
import at.bitfire.davdroid.ui.composable.MultipleChoiceInputDialog
|
||||
import at.bitfire.davdroid.ui.composable.Setting
|
||||
import at.bitfire.davdroid.ui.composable.SettingsHeader
|
||||
import at.bitfire.davdroid.ui.composable.SwitchSetting
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AccountSettingsScreen(
|
||||
onNavUp: () -> Unit,
|
||||
account: Account,
|
||||
onNavWifiPermissionsScreen: () -> Unit,
|
||||
) {
|
||||
val model = hiltViewModel { factory: AccountSettingsModel.Factory ->
|
||||
factory.create(account)
|
||||
}
|
||||
val canAccessWifiSsid by PermissionUtils.rememberCanAccessWifiSsid()
|
||||
|
||||
AppTheme {
|
||||
AccountSettingsScreen(
|
||||
accountName = account.name,
|
||||
onNavUp = onNavUp,
|
||||
|
||||
// Sync settings
|
||||
canAccessWifiSsid = canAccessWifiSsid,
|
||||
onSyncWifiOnlyPermissionsAction = onNavWifiPermissionsScreen,
|
||||
contactsSyncInterval = model.syncIntervalContacts,
|
||||
onUpdateContactsSyncInterval = model::updateContactsSyncInterval,
|
||||
calendarSyncInterval = model.syncIntervalCalendars,
|
||||
onUpdateCalendarSyncInterval = model::updateCalendarSyncInterval,
|
||||
tasksSyncInterval = model.syncIntervalTasks,
|
||||
onUpdateTasksSyncInterval = model::updateTasksSyncInterval,
|
||||
syncOnlyOnWifi = model.syncWifiOnly,
|
||||
onUpdateSyncOnlyOnWifi = model::updateSyncWifiOnly,
|
||||
onlyOnSsids = model.syncWifiOnlySSIDs,
|
||||
onUpdateOnlyOnSsids = model::updateSyncWifiOnlySSIDs,
|
||||
ignoreVpns = model.ignoreVpns,
|
||||
onUpdateIgnoreVpns = model::updateIgnoreVpns,
|
||||
|
||||
// Authentication Settings
|
||||
credentials = model.credentials,
|
||||
onUpdateCredentials = model::updateCredentials,
|
||||
|
||||
// CalDav Settings
|
||||
timeRangePastDays = model.timeRangePastDays,
|
||||
onUpdateTimeRangePastDays = model::updateTimeRangePastDays,
|
||||
defaultAlarmMinBefore = model.defaultAlarmMinBefore,
|
||||
onUpdateDefaultAlarmMinBefore = model::updateDefaultAlarm,
|
||||
manageCalendarColors = model.manageCalendarColors,
|
||||
onUpdateManageCalendarColors = model::updateManageCalendarColors,
|
||||
eventColors = model.eventColors,
|
||||
onUpdateEventColors = model::updateEventColors,
|
||||
|
||||
// CardDav Settings
|
||||
contactGroupMethod = model.contactGroupMethod,
|
||||
onUpdateContactGroupMethod = model::updateContactGroupMethod,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountSettingsScreen(
|
||||
onNavUp: () -> Unit,
|
||||
accountName: String,
|
||||
|
||||
// Sync settings
|
||||
canAccessWifiSsid: Boolean,
|
||||
onSyncWifiOnlyPermissionsAction: () -> Unit,
|
||||
contactsSyncInterval: Long?,
|
||||
onUpdateContactsSyncInterval: ((Long) -> Unit) = {},
|
||||
calendarSyncInterval: Long?,
|
||||
onUpdateCalendarSyncInterval: ((Long) -> Unit) = {},
|
||||
tasksSyncInterval: Long?,
|
||||
onUpdateTasksSyncInterval: ((Long) -> Unit) = {},
|
||||
syncOnlyOnWifi: Boolean,
|
||||
onUpdateSyncOnlyOnWifi: (Boolean) -> Unit = {},
|
||||
onlyOnSsids: List<String>?,
|
||||
onUpdateOnlyOnSsids: (List<String>) -> Unit = {},
|
||||
ignoreVpns: Boolean,
|
||||
onUpdateIgnoreVpns: (Boolean) -> Unit = {},
|
||||
|
||||
// Authentication Settings
|
||||
credentials: Credentials?,
|
||||
onUpdateCredentials: (Credentials) -> Unit = {},
|
||||
|
||||
// CalDav Settings
|
||||
timeRangePastDays: Int?,
|
||||
onUpdateTimeRangePastDays: (Int?) -> Unit = {},
|
||||
defaultAlarmMinBefore: Int?,
|
||||
onUpdateDefaultAlarmMinBefore: (Int?) -> Unit = {},
|
||||
manageCalendarColors: Boolean,
|
||||
onUpdateManageCalendarColors: (Boolean) -> Unit = {},
|
||||
eventColors: Boolean,
|
||||
onUpdateEventColors: (Boolean) -> Unit = {},
|
||||
|
||||
// CardDav Settings
|
||||
contactGroupMethod: GroupMethod,
|
||||
onUpdateContactGroupMethod: (GroupMethod) -> Unit = {},
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavUp) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = stringResource(R.string.navigate_up)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = { Text(accountName) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
val settingsUri = Constants.MANUAL_URL.buildUpon()
|
||||
.appendPath(Constants.MANUAL_PATH_SETTINGS)
|
||||
.fragment(Constants.MANUAL_FRAGMENT_ACCOUNT_SETTINGS)
|
||||
.build()
|
||||
uriHandler.openUri(settingsUri.toString())
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.help))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { padding ->
|
||||
Box(
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
AccountSettings_FromModel(
|
||||
snackbarHostState = snackbarHostState,
|
||||
|
||||
// Sync settings
|
||||
canAccessWifiSsid = canAccessWifiSsid,
|
||||
onSyncWifiOnlyPermissionsAction = onSyncWifiOnlyPermissionsAction,
|
||||
contactsSyncInterval = contactsSyncInterval,
|
||||
onUpdateContactsSyncInterval = onUpdateContactsSyncInterval,
|
||||
calendarSyncInterval = calendarSyncInterval,
|
||||
onUpdateCalendarSyncInterval = onUpdateCalendarSyncInterval,
|
||||
taskSyncInterval = tasksSyncInterval,
|
||||
onUpdateTaskSyncInterval = onUpdateTasksSyncInterval,
|
||||
syncOnlyOnWifi = syncOnlyOnWifi,
|
||||
onUpdateSyncOnlyOnWifi = onUpdateSyncOnlyOnWifi,
|
||||
onlyOnSsids = onlyOnSsids,
|
||||
onUpdateOnlyOnSsids = onUpdateOnlyOnSsids,
|
||||
ignoreVpns = ignoreVpns,
|
||||
onUpdateIgnoreVpns = onUpdateIgnoreVpns,
|
||||
|
||||
// Authentication Settings
|
||||
credentials = credentials,
|
||||
onUpdateCredentials = onUpdateCredentials,
|
||||
|
||||
// CalDav Settings
|
||||
timeRangePastDays = timeRangePastDays,
|
||||
onUpdateTimeRangePastDays = onUpdateTimeRangePastDays,
|
||||
defaultAlarmMinBefore = defaultAlarmMinBefore,
|
||||
onUpdateDefaultAlarmMinBefore = onUpdateDefaultAlarmMinBefore,
|
||||
manageCalendarColors = manageCalendarColors,
|
||||
onUpdateManageCalendarColors = onUpdateManageCalendarColors,
|
||||
eventColors = eventColors,
|
||||
onUpdateEventColors = onUpdateEventColors,
|
||||
|
||||
// CardDav Settings
|
||||
contactGroupMethod = contactGroupMethod,
|
||||
onUpdateContactGroupMethod = onUpdateContactGroupMethod,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountSettings_FromModel(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
|
||||
// Sync settings
|
||||
canAccessWifiSsid: Boolean,
|
||||
onSyncWifiOnlyPermissionsAction: () -> Unit,
|
||||
contactsSyncInterval: Long?,
|
||||
onUpdateContactsSyncInterval: ((Long) -> Unit) = {},
|
||||
calendarSyncInterval: Long?,
|
||||
onUpdateCalendarSyncInterval: ((Long) -> Unit) = {},
|
||||
taskSyncInterval: Long?,
|
||||
onUpdateTaskSyncInterval: ((Long) -> Unit) = {},
|
||||
syncOnlyOnWifi: Boolean,
|
||||
onUpdateSyncOnlyOnWifi: (Boolean) -> Unit = {},
|
||||
onlyOnSsids: List<String>?,
|
||||
onUpdateOnlyOnSsids: (List<String>) -> Unit = {},
|
||||
ignoreVpns: Boolean,
|
||||
onUpdateIgnoreVpns: (Boolean) -> Unit = {},
|
||||
|
||||
// Authentication Settings
|
||||
credentials: Credentials?,
|
||||
onUpdateCredentials: (Credentials) -> Unit = {},
|
||||
|
||||
// CalDav Settings
|
||||
timeRangePastDays: Int?,
|
||||
onUpdateTimeRangePastDays: (Int?) -> Unit = {},
|
||||
defaultAlarmMinBefore: Int?,
|
||||
onUpdateDefaultAlarmMinBefore: (Int?) -> Unit = {},
|
||||
manageCalendarColors: Boolean,
|
||||
onUpdateManageCalendarColors: (Boolean) -> Unit = {},
|
||||
eventColors: Boolean,
|
||||
onUpdateEventColors: (Boolean) -> Unit = {},
|
||||
|
||||
// CardDav Settings
|
||||
contactGroupMethod: GroupMethod,
|
||||
onUpdateContactGroupMethod: (GroupMethod) -> Unit = {},
|
||||
) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
SyncSettings(
|
||||
canAccessWifiSsid = canAccessWifiSsid,
|
||||
onSyncWifiOnlyPermissionsAction = onSyncWifiOnlyPermissionsAction,
|
||||
contactsSyncInterval = contactsSyncInterval,
|
||||
onUpdateContactsSyncInterval = onUpdateContactsSyncInterval,
|
||||
calendarSyncInterval = calendarSyncInterval,
|
||||
onUpdateCalendarSyncInterval = onUpdateCalendarSyncInterval,
|
||||
taskSyncInterval = taskSyncInterval,
|
||||
onUpdateTaskSyncInterval = onUpdateTaskSyncInterval,
|
||||
syncOnlyOnWifi = syncOnlyOnWifi,
|
||||
onUpdateSyncOnlyOnWifi = onUpdateSyncOnlyOnWifi,
|
||||
onlyOnSsids = onlyOnSsids,
|
||||
onUpdateOnlyOnSsids = onUpdateOnlyOnSsids,
|
||||
ignoreVpns = ignoreVpns,
|
||||
onUpdateIgnoreVpns = onUpdateIgnoreVpns
|
||||
)
|
||||
|
||||
credentials?.let {
|
||||
AuthenticationSettings(
|
||||
snackbarHostState = snackbarHostState,
|
||||
credentials = credentials,
|
||||
onUpdateCredentials = onUpdateCredentials
|
||||
)
|
||||
}
|
||||
|
||||
CalDavSettings(
|
||||
timeRangePastDays = timeRangePastDays,
|
||||
onUpdateTimeRangePastDays = onUpdateTimeRangePastDays,
|
||||
defaultAlarmMinBefore = defaultAlarmMinBefore,
|
||||
onUpdateDefaultAlarmMinBefore = onUpdateDefaultAlarmMinBefore,
|
||||
manageCalendarColors = manageCalendarColors,
|
||||
onUpdateManageCalendarColors = onUpdateManageCalendarColors,
|
||||
eventColors = eventColors,
|
||||
onUpdateEventColors = onUpdateEventColors,
|
||||
)
|
||||
|
||||
CardDavSettings(
|
||||
contactGroupMethod = contactGroupMethod,
|
||||
onUpdateContactGroupMethod = onUpdateContactGroupMethod
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SyncSettings(
|
||||
canAccessWifiSsid: Boolean,
|
||||
onSyncWifiOnlyPermissionsAction: () -> Unit,
|
||||
contactsSyncInterval: Long?,
|
||||
onUpdateContactsSyncInterval: ((Long) -> Unit) = {},
|
||||
calendarSyncInterval: Long?,
|
||||
onUpdateCalendarSyncInterval: ((Long) -> Unit) = {},
|
||||
taskSyncInterval: Long?,
|
||||
onUpdateTaskSyncInterval: ((Long) -> Unit) = {},
|
||||
syncOnlyOnWifi: Boolean,
|
||||
onUpdateSyncOnlyOnWifi: (Boolean) -> Unit = {},
|
||||
onlyOnSsids: List<String>?,
|
||||
onUpdateOnlyOnSsids: (List<String>) -> Unit = {},
|
||||
ignoreVpns: Boolean,
|
||||
onUpdateIgnoreVpns: (Boolean) -> Unit = {}
|
||||
) {
|
||||
Column {
|
||||
SettingsHeader(false) {
|
||||
Text(stringResource(R.string.settings_sync))
|
||||
}
|
||||
|
||||
if (contactsSyncInterval != null)
|
||||
SyncIntervalSetting(
|
||||
icon = Icons.Default.Contacts,
|
||||
name = R.string.settings_sync_interval_contacts,
|
||||
syncInterval = contactsSyncInterval,
|
||||
onUpdateSyncInterval = onUpdateContactsSyncInterval
|
||||
)
|
||||
if (calendarSyncInterval != null)
|
||||
SyncIntervalSetting(
|
||||
icon = Icons.Default.Event,
|
||||
name = R.string.settings_sync_interval_calendars,
|
||||
syncInterval = calendarSyncInterval,
|
||||
onUpdateSyncInterval = onUpdateCalendarSyncInterval
|
||||
)
|
||||
if (taskSyncInterval != null)
|
||||
SyncIntervalSetting(
|
||||
icon = Icons.Outlined.Task,
|
||||
name = R.string.settings_sync_interval_tasks,
|
||||
syncInterval = taskSyncInterval,
|
||||
onUpdateSyncInterval = onUpdateTaskSyncInterval
|
||||
)
|
||||
|
||||
SwitchSetting(
|
||||
icon = Icons.Default.Wifi,
|
||||
name = stringResource(R.string.settings_sync_wifi_only),
|
||||
summaryOn = stringResource(R.string.settings_sync_wifi_only_on),
|
||||
summaryOff = stringResource(R.string.settings_sync_wifi_only_off),
|
||||
checked = syncOnlyOnWifi,
|
||||
onCheckedChange = onUpdateSyncOnlyOnWifi
|
||||
)
|
||||
|
||||
var showWifiOnlySsidsDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_sync_wifi_only_ssids),
|
||||
enabled = syncOnlyOnWifi,
|
||||
summary =
|
||||
if (onlyOnSsids != null)
|
||||
stringResource(R.string.settings_sync_wifi_only_ssids_on, onlyOnSsids.joinToString(", "))
|
||||
else
|
||||
stringResource(R.string.settings_sync_wifi_only_ssids_off),
|
||||
onClick = {
|
||||
showWifiOnlySsidsDialog = true
|
||||
}
|
||||
)
|
||||
if (showWifiOnlySsidsDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_sync_wifi_only_ssids_message),
|
||||
initialValue = onlyOnSsids?.joinToString(", ") ?: "",
|
||||
onValueEntered = { newValue ->
|
||||
val newSsids = newValue.split(',')
|
||||
.map { it.trim() }
|
||||
.distinct()
|
||||
onUpdateOnlyOnSsids(newSsids)
|
||||
showWifiOnlySsidsDialog = false
|
||||
},
|
||||
onDismiss = { showWifiOnlySsidsDialog = false }
|
||||
)
|
||||
|
||||
if (LocalInspectionMode.current || (onlyOnSsids != null && !canAccessWifiSsid))
|
||||
ActionCard(
|
||||
icon = Icons.Default.SyncProblem,
|
||||
actionText = stringResource(R.string.settings_sync_wifi_only_ssids_permissions_action),
|
||||
onAction = onSyncWifiOnlyPermissionsAction
|
||||
) {
|
||||
Text(stringResource(R.string.settings_sync_wifi_only_ssids_permissions_required))
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_ignore_vpns),
|
||||
summaryOn = stringResource(R.string.settings_ignore_vpns_on),
|
||||
summaryOff = stringResource(R.string.settings_ignore_vpns_off),
|
||||
checked = ignoreVpns,
|
||||
onCheckedChange = onUpdateIgnoreVpns
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SyncIntervalSetting(
|
||||
icon: ImageVector,
|
||||
@StringRes name: Int,
|
||||
syncInterval: Long,
|
||||
onUpdateSyncInterval: (Long) -> Unit
|
||||
) {
|
||||
var showSyncIntervalDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = icon,
|
||||
name = stringResource(name),
|
||||
summary =
|
||||
if (syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
stringResource(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
stringResource(R.string.settings_sync_summary_periodically, syncInterval / 60),
|
||||
onClick = {
|
||||
showSyncIntervalDialog = true
|
||||
}
|
||||
)
|
||||
if (showSyncIntervalDialog) {
|
||||
val syncIntervalNames = stringArrayResource(R.array.settings_sync_interval_names)
|
||||
val syncIntervalSeconds = stringArrayResource(R.array.settings_sync_interval_seconds)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(name),
|
||||
namesAndValues = syncIntervalNames.zip(syncIntervalSeconds),
|
||||
initialValue = syncInterval.toString(),
|
||||
onValueSelected = { newValue ->
|
||||
try {
|
||||
val seconds = newValue.toLong()
|
||||
onUpdateSyncInterval(seconds)
|
||||
} catch (_: NumberFormatException) {
|
||||
}
|
||||
showSyncIntervalDialog = false
|
||||
},
|
||||
onDismiss = {
|
||||
showSyncIntervalDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthenticationSettings(
|
||||
credentials: Credentials,
|
||||
snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
onUpdateCredentials: (Credentials) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (credentials.authState != null || credentials.username != null || credentials.password != null || credentials.certificateAlias != null)
|
||||
Column {
|
||||
SettingsHeader(false) {
|
||||
Text(stringResource(R.string.settings_authentication))
|
||||
}
|
||||
|
||||
if (credentials.username != null || credentials.password != null) {
|
||||
var showUsernameDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.AccountCircle,
|
||||
name = stringResource(R.string.settings_username),
|
||||
summary = credentials.username,
|
||||
onClick = {
|
||||
showUsernameDialog = true
|
||||
}
|
||||
)
|
||||
if (showUsernameDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_username),
|
||||
initialValue = credentials.username,
|
||||
onValueEntered = { newValue ->
|
||||
onUpdateCredentials(credentials.copy(username = newValue))
|
||||
},
|
||||
onDismiss = { showUsernameDialog = false }
|
||||
)
|
||||
|
||||
var showPasswordDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.Password,
|
||||
name = stringResource(R.string.settings_password),
|
||||
summary = stringResource(R.string.settings_password_summary),
|
||||
onClick = {
|
||||
showPasswordDialog = true
|
||||
}
|
||||
)
|
||||
if (showPasswordDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_password),
|
||||
inputLabel = stringResource(R.string.settings_new_password),
|
||||
initialValue = null, // Do not show the existing password
|
||||
passwordField = true,
|
||||
onValueEntered = { newValue ->
|
||||
onUpdateCredentials(credentials.copy(password = newValue))
|
||||
},
|
||||
onDismiss = { showPasswordDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
// client certificate
|
||||
Setting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_certificate_alias),
|
||||
summary = credentials.certificateAlias ?: stringResource(R.string.settings_certificate_alias_empty),
|
||||
onClick = {
|
||||
val activity = context as Activity
|
||||
KeyChain.choosePrivateKeyAlias(activity, { newAlias ->
|
||||
if (newAlias != null)
|
||||
onUpdateCredentials(credentials.copy(certificateAlias = newAlias))
|
||||
else
|
||||
scope.launch {
|
||||
if (snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.settings_certificate_alias_empty),
|
||||
actionLabel = context.getString(R.string.settings_certificate_install)
|
||||
) == SnackbarResult.ActionPerformed) {
|
||||
val intent = KeyChain.createInstallIntent()
|
||||
if (intent.resolveActivity(context.packageManager) != null)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}, null, null, null, -1, credentials.certificateAlias)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CalDavSettings(
|
||||
timeRangePastDays: Int?,
|
||||
onUpdateTimeRangePastDays: (Int?) -> Unit = {},
|
||||
defaultAlarmMinBefore: Int?,
|
||||
onUpdateDefaultAlarmMinBefore: (Int?) -> Unit = {},
|
||||
manageCalendarColors: Boolean,
|
||||
onUpdateManageCalendarColors: (Boolean) -> Unit = {},
|
||||
eventColors: Boolean,
|
||||
onUpdateEventColors: (Boolean) -> Unit = {}
|
||||
) {
|
||||
Column {
|
||||
SettingsHeader {
|
||||
Text(stringResource(R.string.settings_caldav))
|
||||
}
|
||||
|
||||
var showTimeRangePastDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.History,
|
||||
name = stringResource(R.string.settings_sync_time_range_past),
|
||||
summary =
|
||||
if (timeRangePastDays != null)
|
||||
pluralStringResource(R.plurals.settings_sync_time_range_past_days, timeRangePastDays, timeRangePastDays)
|
||||
else
|
||||
stringResource(R.string.settings_sync_time_range_past_none),
|
||||
onClick = {
|
||||
showTimeRangePastDialog = true
|
||||
}
|
||||
)
|
||||
if (showTimeRangePastDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_sync_time_range_past_message),
|
||||
initialValue = timeRangePastDays?.toString() ?: "",
|
||||
onValueEntered = { newValue ->
|
||||
val days = try {
|
||||
newValue.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
onUpdateTimeRangePastDays(days)
|
||||
showTimeRangePastDialog = false
|
||||
},
|
||||
onDismiss = { showTimeRangePastDialog = false }
|
||||
)
|
||||
|
||||
var showDefaultAlarmDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_default_alarm),
|
||||
summary =
|
||||
if (defaultAlarmMinBefore != null)
|
||||
pluralStringResource(R.plurals.settings_default_alarm_on, defaultAlarmMinBefore, defaultAlarmMinBefore)
|
||||
else
|
||||
stringResource(R.string.settings_default_alarm_off),
|
||||
onClick = {
|
||||
showDefaultAlarmDialog = true
|
||||
}
|
||||
)
|
||||
if (showDefaultAlarmDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.settings_default_alarm_message),
|
||||
initialValue = defaultAlarmMinBefore?.toString() ?: "",
|
||||
onValueEntered = { newValue ->
|
||||
val minBefore = try {
|
||||
newValue.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
onUpdateDefaultAlarmMinBefore(minBefore)
|
||||
showDefaultAlarmDialog = false
|
||||
},
|
||||
onDismiss = { showDefaultAlarmDialog = false }
|
||||
)
|
||||
|
||||
SwitchSetting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_manage_calendar_colors),
|
||||
summaryOn = stringResource(R.string.settings_manage_calendar_colors_on),
|
||||
summaryOff = stringResource(R.string.settings_manage_calendar_colors_off),
|
||||
checked = manageCalendarColors,
|
||||
onCheckedChange = onUpdateManageCalendarColors
|
||||
)
|
||||
|
||||
SwitchSetting(
|
||||
icon = null,
|
||||
name = stringResource(R.string.settings_event_colors),
|
||||
summaryOn = stringResource(R.string.settings_event_colors_on),
|
||||
summaryOff = stringResource(R.string.settings_event_colors_off),
|
||||
checked = eventColors,
|
||||
onCheckedChange = onUpdateEventColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CardDavSettings(
|
||||
contactGroupMethod: GroupMethod,
|
||||
onUpdateContactGroupMethod: (GroupMethod) -> Unit = {}
|
||||
) {
|
||||
Column {
|
||||
SettingsHeader {
|
||||
Text(stringResource(R.string.settings_carddav))
|
||||
}
|
||||
|
||||
val groupMethodNames = stringArrayResource(R.array.settings_contact_group_method_entries)
|
||||
val groupMethodValues = stringArrayResource(R.array.settings_contact_group_method_values)
|
||||
var showGroupMethodDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
icon = Icons.Default.Contacts,
|
||||
name = stringResource(R.string.settings_contact_group_method),
|
||||
summary = groupMethodNames[groupMethodValues.indexOf(contactGroupMethod.name)],
|
||||
onClick = {
|
||||
showGroupMethodDialog = true
|
||||
}
|
||||
)
|
||||
if (showGroupMethodDialog)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(R.string.settings_contact_group_method),
|
||||
namesAndValues = groupMethodNames.zip(groupMethodValues),
|
||||
initialValue = contactGroupMethod.name,
|
||||
onValueSelected = { newValue ->
|
||||
onUpdateContactGroupMethod(GroupMethod.valueOf(newValue))
|
||||
showGroupMethodDialog = false
|
||||
},
|
||||
onDismiss = { showGroupMethodDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AccountSettingsScreen_Preview() {
|
||||
AppTheme {
|
||||
AccountSettingsScreen(
|
||||
accountName = "Account Name Here",
|
||||
onNavUp = {},
|
||||
|
||||
// Sync settings
|
||||
canAccessWifiSsid = true,
|
||||
onSyncWifiOnlyPermissionsAction = {},
|
||||
contactsSyncInterval = 80000L,
|
||||
onUpdateContactsSyncInterval = {},
|
||||
calendarSyncInterval = 50000L,
|
||||
onUpdateCalendarSyncInterval = {},
|
||||
tasksSyncInterval = 900000L,
|
||||
onUpdateTasksSyncInterval = {},
|
||||
syncOnlyOnWifi = true,
|
||||
onUpdateSyncOnlyOnWifi = {},
|
||||
onlyOnSsids = listOf("HeyWifi", "Another"),
|
||||
onUpdateOnlyOnSsids = {},
|
||||
ignoreVpns = true,
|
||||
onUpdateIgnoreVpns = {},
|
||||
|
||||
// Authentication Settings
|
||||
credentials = Credentials(username = "test", password = "test"),
|
||||
onUpdateCredentials = {},
|
||||
|
||||
// CalDav Settings
|
||||
timeRangePastDays = 365,
|
||||
onUpdateTimeRangePastDays = {},
|
||||
defaultAlarmMinBefore = 585,
|
||||
onUpdateDefaultAlarmMinBefore = {},
|
||||
manageCalendarColors = false,
|
||||
onUpdateManageCalendarColors = {},
|
||||
eventColors = false,
|
||||
onUpdateEventColors = {},
|
||||
|
||||
// CardDav Settings
|
||||
contactGroupMethod = GroupMethod.GROUP_VCARDS,
|
||||
onUpdateContactGroupMethod = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -336,8 +336,6 @@
|
|||
<string name="settings_ignore_vpns_on">VPN without underlying validated Internet connection is not enough to run synchronization (recommended)</string>
|
||||
<string name="settings_ignore_vpns_off">VPN without underlying validated Internet connection is enough to run synchronization</string>
|
||||
<string name="settings_authentication">Authentication</string>
|
||||
<string name="settings_oauth">Re-authenticate</string>
|
||||
<string name="settings_oauth_summary">Perform OAuth login again</string>
|
||||
<string name="settings_username">User name</string>
|
||||
<string name="settings_password">Password</string>
|
||||
<string name="settings_new_password">New password</string>
|
||||
|
|
Loading…
Reference in New Issue
Block a user