mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-22 11:11:02 +00:00
Rewrite AccountSettingsActivity to Compose (#646)
* AccountSettings: rewrite Sync settings * Authentication * CalDAV, CardDAV settings
This commit is contained in:
parent
45d6b33023
commit
2e780a890b
|
@ -153,12 +153,13 @@
|
|||
android:label="@string/create_calendar"
|
||||
android:parentActivityName=".ui.account.AccountActivity" />
|
||||
<activity
|
||||
android:name=".ui.account.SettingsActivity"
|
||||
android:name=".ui.account.AccountSettingsActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:parentActivityName=".ui.account.AccountActivity" />
|
||||
<activity
|
||||
android:name=".ui.account.WifiPermissionsActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:parentActivityName=".ui.account.SettingsActivity" />
|
||||
android:parentActivityName=".ui.account.AccountSettingsActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.webdav.WebdavMountsActivity"
|
||||
|
|
|
@ -61,7 +61,7 @@ import at.bitfire.davdroid.settings.SettingsManager
|
|||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
||||
import at.bitfire.davdroid.ui.account.SettingsActivity
|
||||
import at.bitfire.davdroid.ui.account.AccountSettingsActivity
|
||||
import at.bitfire.davdroid.util.DavUtils.parent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
@ -217,8 +217,8 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
} catch (e: UnauthorizedException) {
|
||||
Logger.log.log(Level.SEVERE, "Not authorized (anymore)", e)
|
||||
// notify that we need to re-authenticate in the account settings
|
||||
val settingsIntent = Intent(applicationContext, SettingsActivity::class.java)
|
||||
.putExtra(SettingsActivity.EXTRA_ACCOUNT, account)
|
||||
val settingsIntent = Intent(applicationContext, AccountSettingsActivity::class.java)
|
||||
.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account)
|
||||
notifyRefreshError(
|
||||
applicationContext.getString(R.string.sync_error_authentication_failed),
|
||||
settingsIntent
|
||||
|
|
|
@ -51,7 +51,7 @@ import at.bitfire.davdroid.settings.AccountSettings
|
|||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
||||
import at.bitfire.davdroid.ui.account.SettingsActivity
|
||||
import at.bitfire.davdroid.ui.account.AccountSettingsActivity
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.Ical4Android
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
|
@ -779,8 +779,8 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
|
|||
val contentIntent: Intent
|
||||
var viewItemAction: NotificationCompat.Action? = null
|
||||
if (e is UnauthorizedException) {
|
||||
contentIntent = Intent(context, SettingsActivity::class.java)
|
||||
contentIntent.putExtra(SettingsActivity.EXTRA_ACCOUNT,
|
||||
contentIntent = Intent(context, AccountSettingsActivity::class.java)
|
||||
contentIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT,
|
||||
if (authority == ContactsContract.AUTHORITY)
|
||||
mainAccount
|
||||
else
|
||||
|
|
|
@ -92,9 +92,9 @@ class AboutActivity: AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
AppTheme {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
|
|
|
@ -195,8 +195,8 @@ class AccountActivity : AppCompatActivity() {
|
|||
SyncWorker.enqueueAllAuthorities(this, model.account)
|
||||
},
|
||||
onAccountSettings = {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
intent.putExtra(SettingsActivity.EXTRA_ACCOUNT, model.account)
|
||||
val intent = Intent(this, AccountSettingsActivity::class.java)
|
||||
intent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, model.account)
|
||||
startActivity(intent, null)
|
||||
},
|
||||
onRenameAccount = { newName ->
|
||||
|
|
|
@ -0,0 +1,830 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
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.resource.TaskUtils
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.Syncer
|
||||
import at.bitfire.davdroid.ui.AppTheme
|
||||
import at.bitfire.davdroid.ui.widget.ActionCard
|
||||
import at.bitfire.davdroid.ui.widget.EditTextInputDialog
|
||||
import at.bitfire.davdroid.ui.widget.MultipleChoiceInputDialog
|
||||
import at.bitfire.davdroid.ui.widget.Setting
|
||||
import at.bitfire.davdroid.ui.widget.SettingsHeader
|
||||
import at.bitfire.davdroid.ui.widget.SwitchSetting
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
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 {
|
||||
AppTheme {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onNavigateUp() }) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun supportShouldUpRecreateTask(targetIntent: Intent) = true
|
||||
|
||||
override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) {
|
||||
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 }
|
||||
)
|
||||
|
||||
// TODO make canAccessWifiSsid live-capable
|
||||
val canAccessWifiSsid =
|
||||
if (LocalInspectionMode.current)
|
||||
false
|
||||
else
|
||||
PermissionUtils.canAccessWifiSsid(context)
|
||||
if (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 = 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),
|
||||
initialValue = credentials.password,
|
||||
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>()
|
||||
|
||||
// TODO tasksProvider LiveData
|
||||
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) SyncWorker.FULL_RESYNC else SyncWorker.RESYNC
|
||||
SyncWorker.enqueue(context, account, authority, expedited = true, resync = resync)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,617 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.security.KeyChain
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.TaskUtils
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.Syncer
|
||||
import at.bitfire.davdroid.ui.UiUtils
|
||||
import at.bitfire.davdroid.ui.setup.GoogleLoginFragment
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
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 org.apache.commons.lang3.StringUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsActivity: AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ACCOUNT = "account"
|
||||
}
|
||||
|
||||
private val account by lazy { intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") }
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
title = getString(R.string.settings_title, account.name)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
// TODO add help button that leads to manual
|
||||
|
||||
if (savedInstanceState == null)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, DialogFragment.instantiate(this, AccountSettingsFragment::class.java.name, intent.extras))
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun supportShouldUpRecreateTask(targetIntent: Intent) = true
|
||||
|
||||
override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) {
|
||||
builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
|
||||
}
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val account by lazy { requireArguments().getParcelable<Account>(EXTRA_ACCOUNT)!! }
|
||||
@Inject lateinit var settings: SettingsManager
|
||||
|
||||
@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 onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.settings_account)
|
||||
|
||||
findPreference<EditTextPreference>(getString(R.string.settings_password_key))!!.setOnBindEditTextListener { password ->
|
||||
password.inputType = InputType.TYPE_CLASS_TEXT.or(InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
try {
|
||||
initSettings()
|
||||
} catch (e: InvalidAccountException) {
|
||||
Toast.makeText(context, R.string.account_invalid, Toast.LENGTH_LONG).show()
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
checkWifiPermissions()
|
||||
}
|
||||
|
||||
private fun initSettings() {
|
||||
// preference group: sync
|
||||
findPreference<ListPreference>(getString(R.string.settings_sync_interval_contacts_key))!!.let {
|
||||
model.syncIntervalContacts.observe(viewLifecycleOwner) { interval: Long? ->
|
||||
if (interval != null) {
|
||||
it.isEnabled = true
|
||||
it.isVisible = true
|
||||
it.value = interval.toString()
|
||||
if (interval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
it.setSummary(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
it.summary = getString(R.string.settings_sync_summary_periodically, interval / 60)
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue ->
|
||||
pref.isEnabled = false // disable until updated setting is read from system again
|
||||
model.updateSyncInterval(getString(R.string.address_books_authority), (newValue as String).toLong())
|
||||
false
|
||||
}
|
||||
} else
|
||||
it.isVisible = false
|
||||
}
|
||||
}
|
||||
findPreference<ListPreference>(getString(R.string.settings_sync_interval_calendars_key))!!.let {
|
||||
model.syncIntervalCalendars.observe(viewLifecycleOwner) { interval: Long? ->
|
||||
if (interval != null) {
|
||||
it.isEnabled = true
|
||||
it.isVisible = true
|
||||
it.value = interval.toString()
|
||||
if (interval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
it.setSummary(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
it.summary = getString(R.string.settings_sync_summary_periodically, interval / 60)
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue ->
|
||||
pref.isEnabled = false
|
||||
model.updateSyncInterval(CalendarContract.AUTHORITY, (newValue as String).toLong())
|
||||
false
|
||||
}
|
||||
} else
|
||||
it.isVisible = false
|
||||
}
|
||||
}
|
||||
findPreference<ListPreference>(getString(R.string.settings_sync_interval_tasks_key))!!.let {
|
||||
model.syncIntervalTasks.observe(viewLifecycleOwner) { interval: Long? ->
|
||||
val provider = model.tasksProvider
|
||||
if (provider != null && interval != null) {
|
||||
it.isEnabled = true
|
||||
it.isVisible = true
|
||||
it.value = interval.toString()
|
||||
if (interval == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||
it.setSummary(R.string.settings_sync_summary_manually)
|
||||
else
|
||||
it.summary = getString(R.string.settings_sync_summary_periodically, interval / 60)
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { pref, newValue ->
|
||||
pref.isEnabled = false
|
||||
model.updateSyncInterval(provider.authority, (newValue as String).toLong())
|
||||
false
|
||||
}
|
||||
} else
|
||||
it.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<SwitchPreferenceCompat>(getString(R.string.settings_sync_wifi_only_key))!!.let {
|
||||
model.syncWifiOnly.observe(viewLifecycleOwner) { wifiOnly ->
|
||||
it.isEnabled = !settings.containsKey(AccountSettings.KEY_WIFI_ONLY)
|
||||
it.isChecked = wifiOnly
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, wifiOnly ->
|
||||
model.updateSyncWifiOnly(wifiOnly as Boolean)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<EditTextPreference>(getString(R.string.settings_sync_wifi_only_ssids_key))!!.let {
|
||||
model.syncWifiOnly.observe(viewLifecycleOwner) { wifiOnly ->
|
||||
it.isEnabled = wifiOnly && settings.isWritable(AccountSettings.KEY_WIFI_ONLY_SSIDS)
|
||||
}
|
||||
model.syncWifiOnlySSIDs.observe(viewLifecycleOwner) { onlySSIDs ->
|
||||
checkWifiPermissions()
|
||||
|
||||
if (onlySSIDs != null) {
|
||||
it.text = onlySSIDs.joinToString(", ")
|
||||
it.summary = getString(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
R.string.settings_sync_wifi_only_ssids_on_location_services
|
||||
else R.string.settings_sync_wifi_only_ssids_on, onlySSIDs.joinToString(", "))
|
||||
} else {
|
||||
it.text = ""
|
||||
it.setSummary(R.string.settings_sync_wifi_only_ssids_off)
|
||||
}
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val newOnlySSIDs = (newValue as String)
|
||||
.split(',')
|
||||
.mapNotNull { StringUtils.trimToNull(it) }
|
||||
.distinct()
|
||||
model.updateSyncWifiOnlySSIDs(newOnlySSIDs)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<SwitchPreferenceCompat>(getString(R.string.settings_ignore_vpns_key))!!.let {
|
||||
model.ignoreVpns.observe(viewLifecycleOwner) { ignoreVpns ->
|
||||
it.isEnabled = true
|
||||
it.isChecked = ignoreVpns
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, prefValue ->
|
||||
model.updateIgnoreVpns(prefValue as Boolean)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preference group: authentication
|
||||
val prefUserName = findPreference<EditTextPreference>(getString(R.string.settings_username_key))!!
|
||||
val prefPassword = findPreference<EditTextPreference>(getString(R.string.settings_password_key))!!
|
||||
val prefCertAlias = findPreference<Preference>(getString(R.string.settings_certificate_alias_key))!!
|
||||
val prefOAuth = findPreference<Preference>(getString(R.string.settings_oauth_key))!!
|
||||
|
||||
model.credentials.observe(viewLifecycleOwner) { credentials ->
|
||||
if (credentials.authState != null) {
|
||||
// using OAuth, hide other settings
|
||||
prefOAuth.isVisible = true
|
||||
prefUserName.isVisible = false
|
||||
prefPassword.isVisible = false
|
||||
prefCertAlias.isVisible = false
|
||||
|
||||
prefOAuth.setOnPreferenceClickListener {
|
||||
parentFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, GoogleLoginFragment(account.name), null)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// not using OAuth, hide OAuth setting, show the others
|
||||
prefOAuth.isVisible = false
|
||||
prefUserName.isVisible = true
|
||||
prefPassword.isVisible = true
|
||||
prefCertAlias.isVisible = true
|
||||
|
||||
prefUserName.summary = credentials.userName
|
||||
prefUserName.text = credentials.userName
|
||||
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newUserName ->
|
||||
val newUserNameOrNull = StringUtils.trimToNull(newUserName as String)
|
||||
model.updateCredentials(Credentials(
|
||||
userName = newUserNameOrNull,
|
||||
password = credentials.password,
|
||||
certificateAlias = credentials.certificateAlias)
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
if (credentials.userName != null) {
|
||||
prefPassword.isVisible = true
|
||||
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newPassword ->
|
||||
model.updateCredentials(Credentials(credentials.userName, newPassword as String, credentials.certificateAlias))
|
||||
false
|
||||
}
|
||||
} else
|
||||
prefPassword.isVisible = false
|
||||
|
||||
prefCertAlias.summary = credentials.certificateAlias ?: getString(R.string.settings_certificate_alias_empty)
|
||||
prefCertAlias.setOnPreferenceClickListener {
|
||||
KeyChain.choosePrivateKeyAlias(requireActivity(), { newAlias ->
|
||||
model.updateCredentials(Credentials(credentials.userName, credentials.password, newAlias))
|
||||
}, null, null, null, -1, credentials.certificateAlias)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preference group: CalDAV
|
||||
model.hasCalDav.observe(viewLifecycleOwner) { hasCalDav ->
|
||||
if (!hasCalDav)
|
||||
findPreference<PreferenceGroup>(getString(R.string.settings_caldav_key))!!.isVisible = false
|
||||
else {
|
||||
findPreference<PreferenceGroup>(getString(R.string.settings_caldav_key))!!.isVisible = true
|
||||
|
||||
// when model.hasCalDav is available, model.syncInterval* are also available
|
||||
// (because hasCalDav is calculated from them)
|
||||
val hasCalendars = model.syncIntervalCalendars.value != null
|
||||
|
||||
findPreference<EditTextPreference>(getString(R.string.settings_sync_time_range_past_key))!!.let { pref ->
|
||||
if (hasCalendars)
|
||||
model.timeRangePastDays.observe(viewLifecycleOwner) { pastDays ->
|
||||
if (model.syncIntervalCalendars.value != null) {
|
||||
pref.isVisible = true
|
||||
if (pastDays != null) {
|
||||
pref.text = pastDays.toString()
|
||||
pref.summary = resources.getQuantityString(R.plurals.settings_sync_time_range_past_days, pastDays, pastDays)
|
||||
} else {
|
||||
pref.text = null
|
||||
pref.setSummary(R.string.settings_sync_time_range_past_none)
|
||||
}
|
||||
pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val days = try {
|
||||
(newValue as String).toInt()
|
||||
} catch(e: NumberFormatException) {
|
||||
-1
|
||||
}
|
||||
model.updateTimeRangePastDays(if (days < 0) null else days)
|
||||
false
|
||||
}
|
||||
} else
|
||||
pref.isVisible = false
|
||||
}
|
||||
else
|
||||
pref.isVisible = false
|
||||
}
|
||||
|
||||
findPreference<EditTextPreference>(getString(R.string.settings_key_default_alarm))!!.let { pref ->
|
||||
if (hasCalendars)
|
||||
model.defaultAlarmMinBefore.observe(viewLifecycleOwner) { minBefore ->
|
||||
pref.isVisible = true
|
||||
if (minBefore != null) {
|
||||
pref.text = minBefore.toString()
|
||||
pref.summary = resources.getQuantityString(R.plurals.settings_default_alarm_on, minBefore, minBefore)
|
||||
} else {
|
||||
pref.text = null
|
||||
pref.summary = getString(R.string.settings_default_alarm_off)
|
||||
}
|
||||
pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val minBefore = try {
|
||||
(newValue as String).toInt()
|
||||
} catch (e: java.lang.NumberFormatException) {
|
||||
null
|
||||
}
|
||||
model.updateDefaultAlarm(minBefore)
|
||||
false
|
||||
}
|
||||
}
|
||||
else
|
||||
pref.isVisible = false
|
||||
}
|
||||
|
||||
findPreference<SwitchPreferenceCompat>(getString(R.string.settings_manage_calendar_colors_key))!!.let {
|
||||
model.manageCalendarColors.observe(viewLifecycleOwner) { manageCalendarColors ->
|
||||
it.isEnabled = !settings.containsKey(AccountSettings.KEY_MANAGE_CALENDAR_COLORS)
|
||||
it.isChecked = manageCalendarColors
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
model.updateManageCalendarColors(newValue as Boolean)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<SwitchPreferenceCompat>(getString(R.string.settings_event_colors_key))!!.let { pref ->
|
||||
if (hasCalendars)
|
||||
model.eventColors.observe(viewLifecycleOwner) { eventColors ->
|
||||
pref.isVisible = true
|
||||
pref.isEnabled = !settings.containsKey(AccountSettings.KEY_EVENT_COLORS)
|
||||
pref.isChecked = eventColors
|
||||
pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
model.updateEventColors(newValue as Boolean)
|
||||
false
|
||||
}
|
||||
}
|
||||
else
|
||||
pref.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preference group: CardDAV
|
||||
model.syncIntervalContacts.observe(viewLifecycleOwner) { contactsSyncInterval ->
|
||||
val hasCardDav = contactsSyncInterval != null
|
||||
if (!hasCardDav)
|
||||
findPreference<PreferenceGroup>(getString(R.string.settings_carddav_key))!!.isVisible = false
|
||||
else {
|
||||
findPreference<PreferenceGroup>(getString(R.string.settings_carddav_key))!!.isVisible = true
|
||||
findPreference<ListPreference>(getString(R.string.settings_contact_group_method_key))!!.let {
|
||||
model.contactGroupMethod.observe(viewLifecycleOwner) { groupMethod ->
|
||||
if (model.syncIntervalContacts.value != null) {
|
||||
it.isVisible = true
|
||||
it.value = groupMethod.name
|
||||
it.summary = it.entry
|
||||
if (settings.containsKey(AccountSettings.KEY_CONTACT_GROUP_METHOD))
|
||||
it.isEnabled = false
|
||||
else {
|
||||
it.isEnabled = true
|
||||
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, groupMethod ->
|
||||
model.updateContactGroupMethod(GroupMethod.valueOf(groupMethod as String))
|
||||
false
|
||||
}
|
||||
}
|
||||
} else
|
||||
it.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
private fun checkWifiPermissions() {
|
||||
if (model.syncWifiOnlySSIDs.value != null && !PermissionUtils.canAccessWifiSsid(requireActivity()))
|
||||
Snackbar.make(requireView(), R.string.settings_sync_wifi_only_ssids_permissions_required, UiUtils.SNACKBAR_LENGTH_VERY_LONG)
|
||||
.setAction(R.string.settings_sync_wifi_only_ssids_permissions_action) {
|
||||
val intent = Intent(requireActivity(), WifiPermissionsActivity::class.java)
|
||||
intent.putExtra(WifiPermissionsActivity.EXTRA_ACCOUNT, account)
|
||||
startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Model @AssistedInject constructor(
|
||||
application: Application,
|
||||
val settings: SettingsManager,
|
||||
@Assisted val account: Account
|
||||
): AndroidViewModel(application), 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(application)
|
||||
val syncIntervalTasks = MutableLiveData<Long>()
|
||||
val hasCalDav = object: MediatorLiveData<Boolean>() {
|
||||
init {
|
||||
addSource(syncIntervalCalendars) { calculate() }
|
||||
addSource(syncIntervalTasks) { calculate() }
|
||||
}
|
||||
private fun calculate() {
|
||||
value = syncIntervalCalendars.value != null || syncIntervalTasks.value != null
|
||||
}
|
||||
}
|
||||
|
||||
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(application, 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(getApplication<Application>().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 = getApplication<Application>().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) SyncWorker.FULL_RESYNC else SyncWorker.RESYNC
|
||||
SyncWorker.enqueue(getApplication(), account, authority, expedited = true, resync = resync)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -120,7 +120,7 @@ class WifiPermissionsActivity: AppCompatActivity() {
|
|||
override fun supportShouldUpRecreateTask(targetIntent: Intent) = true
|
||||
|
||||
override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) {
|
||||
builder.editIntentAt(builder.intentCount - 1)?.putExtra(SettingsActivity.EXTRA_ACCOUNT, account)
|
||||
builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
@ -131,7 +130,6 @@ class AddWebdavMountActivity : AppCompatActivity() {
|
|||
password: String = "",
|
||||
onPasswordChange: (String) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -35,16 +36,21 @@ fun ActionCard(
|
|||
.fillMaxWidth()
|
||||
.then(modifier)
|
||||
) {
|
||||
Column(Modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp)) {
|
||||
if (icon != null)
|
||||
Row {
|
||||
Icon(icon, "", Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(end = 8.dp))
|
||||
Column(Modifier
|
||||
.padding(top = 8.dp, start = 8.dp, end = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
ProvideTextStyle(MaterialTheme.typography.body1) {
|
||||
if (icon != null)
|
||||
Row {
|
||||
Icon(icon, "", Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(end = 8.dp))
|
||||
content()
|
||||
}
|
||||
else
|
||||
content()
|
||||
}
|
||||
else
|
||||
content()
|
||||
}
|
||||
|
||||
if (actionText != null)
|
||||
TextButton(onClick = onAction) {
|
||||
|
|
|
@ -50,7 +50,7 @@ fun EditTextInputDialog(
|
|||
title = {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.h6
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
},
|
||||
text = {
|
||||
|
@ -123,7 +123,7 @@ fun MultipleChoiceInputDialog(
|
|||
title = {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.h6
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
},
|
||||
text = {
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.material.Divider
|
|||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -19,9 +20,11 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.android.material.color.MaterialColors
|
||||
|
||||
@Composable
|
||||
fun SettingsHeader(divider: Boolean = false, content: @Composable () -> Unit) {
|
||||
|
@ -64,8 +67,10 @@ fun Setting(
|
|||
onClick: () -> Unit = {}
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (enabled)
|
||||
modifier = modifier.clickable(onClick = onClick)
|
||||
modifier = if (enabled)
|
||||
modifier.clickable(onClick = onClick)
|
||||
else
|
||||
modifier.alpha(MaterialColors.ALPHA_DISABLED)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
@ -85,7 +90,9 @@ fun Setting(
|
|||
.padding(start = 8.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
name()
|
||||
ProvideTextStyle(MaterialTheme.typography.body1) {
|
||||
name()
|
||||
}
|
||||
|
||||
if (summary != null)
|
||||
Text(summary, style = MaterialTheme.typography.body2)
|
||||
|
@ -100,6 +107,7 @@ fun Setting(
|
|||
name: String,
|
||||
summary: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Setting(
|
||||
|
@ -111,6 +119,7 @@ fun Setting(
|
|||
Text(name, style = MaterialTheme.typography.body1)
|
||||
},
|
||||
summary = summary,
|
||||
enabled = enabled,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
@ -163,6 +172,7 @@ fun SwitchSetting(
|
|||
fun SwitchSetting_Sample() {
|
||||
SwitchSetting(
|
||||
name = "Some Switched Setting",
|
||||
checked = true
|
||||
checked = true,
|
||||
summaryOn = "Currently on"
|
||||
)
|
||||
}
|
|
@ -310,13 +310,10 @@
|
|||
<!-- AccountSettingsActivity -->
|
||||
<string name="settings_title">Settings: %s</string>
|
||||
<string name="settings_sync">Synchronization</string>
|
||||
<string name="settings_sync_interval_contacts_key" translatable="false">sync_interval_contacts</string>
|
||||
<string name="settings_sync_interval_contacts">Contacts sync. interval</string>
|
||||
<string name="settings_sync_summary_manually">Only manually</string>
|
||||
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Every %d minutes + immediately on local changes</string>
|
||||
<string name="settings_sync_interval_calendars_key" translatable="false">sync_interval_calendars</string>
|
||||
<string name="settings_sync_interval_calendars">Calendars sync. interval</string>
|
||||
<string name="settings_sync_interval_tasks_key" translatable="false">sync_interval_tasks</string>
|
||||
<string name="settings_sync_interval_tasks">Tasks sync. interval</string>
|
||||
<string-array name="settings_sync_interval_seconds" translatable="false">
|
||||
<item>-1</item>
|
||||
|
@ -336,39 +333,30 @@
|
|||
<item>Every 4 hours</item>
|
||||
<item>Once a day</item>
|
||||
</string-array>
|
||||
<string name="settings_sync_wifi_only_key" translatable="false">sync_wifi_only</string>
|
||||
<string name="settings_sync_wifi_only">Sync over WiFi only</string>
|
||||
<string name="settings_sync_wifi_only_on">Synchronization is restricted to WiFi connections</string>
|
||||
<string name="settings_sync_wifi_only_off">Connection type is not taken into consideration</string>
|
||||
<string name="settings_sync_wifi_only_ssids_key" translatable="false">sync_wifi_only_ssids</string>
|
||||
<string name="settings_sync_wifi_only_ssids">WiFi SSID restriction</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on">Will only sync over %s</string>
|
||||
<string name="settings_sync_wifi_only_ssids_on_location_services">Will only sync over %s (requires active location services)</string>
|
||||
<string name="settings_sync_wifi_only_ssids_off">All WiFi connections will be used</string>
|
||||
<string name="settings_sync_wifi_only_ssids_message">Comma-separated names (SSIDs) of allowed WiFi networks (leave blank for all)</string>
|
||||
<string name="settings_sync_wifi_only_ssids_permissions_required">WiFi SSID restriction requires further settings</string>
|
||||
<string name="settings_sync_wifi_only_ssids_permissions_action">Manage</string>
|
||||
<string name="settings_ignore_vpns_key" translatable="false">ignore_vpns</string>
|
||||
<string name="settings_ignore_vpns">VPN requires underlying Internet</string>
|
||||
<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_key" translatable="false">oauth</string>
|
||||
<string name="settings_oauth">Re-authenticate</string>
|
||||
<string name="settings_oauth_summary">Perform OAuth login again</string>
|
||||
<string name="settings_username_key">username</string>
|
||||
<string name="settings_username">User name</string>
|
||||
<string name="settings_enter_username">Enter user name:</string>
|
||||
<string name="settings_password_key" translatable="false">password</string>
|
||||
<string name="settings_password">Password</string>
|
||||
<string name="settings_password_summary">Update the password according to your server.</string>
|
||||
<string name="settings_enter_password">Enter your password:</string>
|
||||
<string name="settings_certificate_alias_key" translatable="false">certificate_alias</string>
|
||||
<string name="settings_certificate_alias">Client certificate alias</string>
|
||||
<string name="settings_certificate_alias_empty">No certificate selected</string>
|
||||
<string name="settings_caldav_key" translatable="false">caldav</string>
|
||||
<string name="settings_certificate_alias">Client certificate</string>
|
||||
<string name="settings_certificate_alias_empty">No certificate available or selected</string>
|
||||
<string name="settings_certificate_install">Install certificate</string>
|
||||
<string name="settings_caldav">CalDAV</string>
|
||||
<string name="settings_sync_time_range_past_key" translatable="false">time_range_past_days</string>
|
||||
<string name="settings_sync_time_range_past">Past event time limit</string>
|
||||
<string name="settings_sync_time_range_past_none">All events will be synchronized</string>
|
||||
<plurals name="settings_sync_time_range_past_days">
|
||||
|
@ -384,17 +372,13 @@
|
|||
</plurals>
|
||||
<string name="settings_default_alarm_off">No default reminders are created</string>
|
||||
<string name="settings_default_alarm_message">If default reminders shall be created for events without reminder: the desired number of minutes before the event. Leave blank to disable default reminders.</string>
|
||||
<string name="settings_manage_calendar_colors_key" translatable="false">manage_calendar_colors</string>
|
||||
<string name="settings_manage_calendar_colors">Manage calendar colors</string>
|
||||
<string name="settings_manage_calendar_colors_on">Calendar colors are reset at each sync</string>
|
||||
<string name="settings_manage_calendar_colors_off">Calendar colors can be set by other apps</string>
|
||||
<string name="settings_event_colors_key" translatable="false">event_colors</string>
|
||||
<string name="settings_event_colors">Event color support</string>
|
||||
<string name="settings_event_colors_on">Event colors are synced</string>
|
||||
<string name="settings_event_colors_off">Event colors are not synced</string>
|
||||
<string name="settings_carddav_key" translatable="false">carddav</string>
|
||||
<string name="settings_carddav">CardDAV</string>
|
||||
<string name="settings_contact_group_method_key" translatable="false">contact_group_method</string>
|
||||
<string name="settings_contact_group_method">Contact group method</string>
|
||||
<string-array name="settings_contact_group_method_values" translatable="false">
|
||||
<item>GROUP_VCARDS</item>
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="sync"
|
||||
android:title="@string/settings_sync">
|
||||
|
||||
<ListPreference
|
||||
android:key="@string/settings_sync_interval_contacts_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_interval_contacts"
|
||||
android:icon="@drawable/ic_contacts"
|
||||
android:entries="@array/settings_sync_interval_names"
|
||||
android:entryValues="@array/settings_sync_interval_seconds"/>
|
||||
|
||||
<ListPreference
|
||||
android:key="@string/settings_sync_interval_calendars_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_interval_calendars"
|
||||
android:icon="@drawable/ic_today"
|
||||
android:entries="@array/settings_sync_interval_names"
|
||||
android:entryValues="@array/settings_sync_interval_seconds"/>
|
||||
|
||||
<ListPreference
|
||||
android:key="@string/settings_sync_interval_tasks_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_interval_tasks"
|
||||
android:icon="@drawable/ic_playlist_add_check"
|
||||
android:entries="@array/settings_sync_interval_names"
|
||||
android:entryValues="@array/settings_sync_interval_seconds"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="@string/settings_sync_wifi_only_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_wifi_only"
|
||||
android:icon="@drawable/ic_network_wifi"
|
||||
android:summaryOn="@string/settings_sync_wifi_only_on"
|
||||
android:summaryOff="@string/settings_sync_wifi_only_off" />
|
||||
|
||||
<EditTextPreference
|
||||
android:key="@string/settings_sync_wifi_only_ssids_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_wifi_only_ssids"
|
||||
android:dialogMessage="@string/settings_sync_wifi_only_ssids_message"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="@string/settings_ignore_vpns_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_ignore_vpns"
|
||||
android:summaryOn="@string/settings_ignore_vpns_on"
|
||||
android:summaryOff="@string/settings_ignore_vpns_off" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="authentication"
|
||||
android:title="@string/settings_authentication">
|
||||
|
||||
<EditTextPreference
|
||||
android:key="@string/settings_username_key"
|
||||
android:title="@string/settings_username"
|
||||
android:icon="@drawable/ic_login"
|
||||
android:persistent="false"
|
||||
android:dialogTitle="@string/settings_enter_username" />
|
||||
|
||||
<EditTextPreference
|
||||
android:key="@string/settings_password_key"
|
||||
android:title="@string/settings_password"
|
||||
android:persistent="false"
|
||||
android:summary="@string/settings_password_summary"
|
||||
android:dialogTitle="@string/settings_enter_password"/>
|
||||
|
||||
<Preference
|
||||
android:key="@string/settings_certificate_alias_key"
|
||||
android:title="@string/settings_certificate_alias"
|
||||
android:persistent="false"/>
|
||||
|
||||
<Preference
|
||||
android:key="@string/settings_oauth_key"
|
||||
android:title="@string/settings_oauth"
|
||||
android:summary="@string/settings_oauth_summary"
|
||||
android:icon="@drawable/ic_login" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/settings_caldav_key"
|
||||
android:title="@string/settings_caldav">
|
||||
|
||||
<EditTextPreference
|
||||
android:key="@string/settings_sync_time_range_past_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_sync_time_range_past"
|
||||
android:icon="@drawable/ic_date_range"
|
||||
android:dialogMessage="@string/settings_sync_time_range_past_message"
|
||||
android:inputType="number"/>
|
||||
|
||||
<EditTextPreference
|
||||
android:key="@string/settings_key_default_alarm"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_default_alarm"
|
||||
android:dialogMessage="@string/settings_default_alarm_message"
|
||||
android:inputType="number"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="@string/settings_manage_calendar_colors_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_manage_calendar_colors"
|
||||
android:summaryOn="@string/settings_manage_calendar_colors_on"
|
||||
android:summaryOff="@string/settings_manage_calendar_colors_off"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="@string/settings_event_colors_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_event_colors"
|
||||
android:summaryOn="@string/settings_event_colors_on"
|
||||
android:summaryOff="@string/settings_event_colors_off"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/settings_carddav_key"
|
||||
android:title="@string/settings_carddav">
|
||||
|
||||
<ListPreference
|
||||
android:key="@string/settings_contact_group_method_key"
|
||||
android:persistent="false"
|
||||
android:title="@string/settings_contact_group_method"
|
||||
android:icon="@drawable/ic_group"
|
||||
android:entries="@array/settings_contact_group_method_entries"
|
||||
android:entryValues="@array/settings_contact_group_method_values"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in a new issue