mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-06 19:34:23 +00:00
Rewrite AppSettingsActivity to M3 (#792)
* Extract composables * Extract model and companion object * Switch to M3 * Linting * Drop previews for sub composables * Minor adjustments for readability * Minor changes - use manual URL from Constants - use M3 in some Composables * Create PreferenceRepository (for now only for verbose logging) * Move actual settings to model; M3 Composables --------- Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
e11d511971
commit
6f02669832
|
@ -24,6 +24,8 @@ object Constants {
|
|||
val MANUAL_URL = "https://manual.davx5.com".toUri()
|
||||
const val MANUAL_PATH_ACCOUNTS_COLLECTIONS = "accounts_collections.html"
|
||||
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_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
|
||||
|
||||
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.repository
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import androidx.preference.PreferenceManager
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Repository to access preferences. Preferences are stored in a shared preferences file
|
||||
* and reflect settings that are very low-level and are therefore not covered by
|
||||
* [at.bitfire.davdroid.settings.SettingsManager].
|
||||
*/
|
||||
class PreferenceRepository @Inject constructor(
|
||||
context: Application
|
||||
) {
|
||||
|
||||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
/**
|
||||
* Updates the "log to file" (verbose logging") setting.
|
||||
*/
|
||||
fun logToFile(logToFile: Boolean) {
|
||||
preferences
|
||||
.edit()
|
||||
.putBoolean(Logger.LOG_TO_FILE, logToFile)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "log to file" (verbose logging) setting as a live value.
|
||||
*/
|
||||
fun logToFileFlow(): Flow<Boolean> = observeAsFlow(Logger.LOG_TO_FILE) {
|
||||
preferences.getBoolean(Logger.LOG_TO_FILE, false)
|
||||
}
|
||||
|
||||
|
||||
private fun<T> observeAsFlow(keyToObserve: String, getValue: () -> T): Flow<T> =
|
||||
callbackFlow {
|
||||
val listener = OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == keyToObserve) {
|
||||
trySend(getValue())
|
||||
}
|
||||
}
|
||||
preferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
|
||||
awaitClose {
|
||||
preferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,546 +5,56 @@
|
|||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.Image
|
||||
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.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.Adb
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.InvertColors
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.SyncProblem
|
||||
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.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.preference.PreferenceManager
|
||||
import at.bitfire.cert4android.CustomCertStore
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
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.ui.intro.BatteryOptimizationsPageModel
|
||||
import at.bitfire.davdroid.ui.intro.OpenSourcePage
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.util.broadcastReceiverFlow
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppSettingsActivity: AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val APP_SETTINGS_HELP_URL = "https://manual.davx5.com/settings.html#app-wide-settings"
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
M2Theme {
|
||||
AppSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
@Composable
|
||||
fun AppSettings(model: Model = viewModel()) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onSupportNavigateUp() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.app_settings)) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
uriHandler.openUri(APP_SETTINGS_HELP_URL)
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.help))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
AppSettings_Debugging(
|
||||
verboseLogging = model.getPrefBoolean(Logger.LOG_TO_FILE).observeAsState().value ?: false,
|
||||
onUpdateVerboseLogging = { model.putPrefBoolean(Logger.LOG_TO_FILE, it) },
|
||||
batterySavingExempted = model.batterySavingExempted.collectAsStateWithLifecycle().value,
|
||||
onExemptFromBatterySaving = {
|
||||
startActivity(Intent(
|
||||
android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
|
||||
))
|
||||
},
|
||||
onBatterySavingSettings = {
|
||||
startActivity(Intent(
|
||||
android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
))
|
||||
}
|
||||
)
|
||||
|
||||
AppSettings_Connection(
|
||||
proxyType = model.settings.getIntFlow(Settings.PROXY_TYPE).collectAsStateWithLifecycle(null).value ?: Settings.PROXY_TYPE_NONE,
|
||||
onProxyTypeUpdated = { model.settings.putInt(Settings.PROXY_TYPE, it) },
|
||||
proxyHostName = model.settings.getStringFlow(Settings.PROXY_HOST).collectAsStateWithLifecycle(null).value,
|
||||
onProxyHostNameUpdated = { model.settings.putString(Settings.PROXY_HOST, it) },
|
||||
proxyPort = model.settings.getIntFlow(Settings.PROXY_PORT).collectAsStateWithLifecycle(null).value,
|
||||
onProxyPortUpdated = { model.settings.putInt(Settings.PROXY_PORT, it) }
|
||||
)
|
||||
|
||||
AppSettings_Security(
|
||||
distrustSystemCerts = model.settings.getBooleanFlow(Settings.DISTRUST_SYSTEM_CERTIFICATES).collectAsStateWithLifecycle(null).value ?: false,
|
||||
onDistrustSystemCertsUpdated = { model.settings.putBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES, it) },
|
||||
onResetCertificates = {
|
||||
model.resetCertificates()
|
||||
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(getString(R.string.app_settings_reset_certificates_success))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
AppSettings_UserInterface(
|
||||
theme = model.settings.getIntFlow(Settings.PREFERRED_THEME).collectAsStateWithLifecycle(null).value ?: Settings.PREFERRED_THEME_DEFAULT,
|
||||
onThemeSelected = {
|
||||
model.settings.putInt(Settings.PREFERRED_THEME, it)
|
||||
UiUtils.updateTheme(context)
|
||||
},
|
||||
onResetHints = {
|
||||
model.resetHints()
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(getString(R.string.app_settings_reset_hints_success))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
AppSettings_Integration(
|
||||
taskProvider = TaskUtils.currentProviderFlow(context, lifecycleScope).collectAsStateWithLifecycle().value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Debugging(
|
||||
verboseLogging: Boolean,
|
||||
onUpdateVerboseLogging: (Boolean) -> Unit,
|
||||
batterySavingExempted: Boolean,
|
||||
onExemptFromBatterySaving: () -> Unit,
|
||||
onBatterySavingSettings: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SettingsHeader {
|
||||
Text(stringResource(R.string.app_settings_debug))
|
||||
}
|
||||
|
||||
Setting(
|
||||
icon = Icons.Default.BugReport,
|
||||
name = stringResource(R.string.app_settings_show_debug_info),
|
||||
summary = stringResource(R.string.app_settings_show_debug_info_details)
|
||||
) {
|
||||
context.startActivity(Intent(context, DebugInfoActivity::class.java))
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
icon = Icons.Default.Adb,
|
||||
checked = verboseLogging,
|
||||
name = stringResource(R.string.app_settings_logging),
|
||||
summaryOn = stringResource(R.string.app_settings_logging_on),
|
||||
summaryOff = stringResource(R.string.app_settings_logging_off)
|
||||
) {
|
||||
onUpdateVerboseLogging(it)
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
checked = batterySavingExempted,
|
||||
icon = Icons.Default.SyncProblem.takeUnless { batterySavingExempted },
|
||||
name = stringResource(R.string.app_settings_battery_optimization),
|
||||
summaryOn = stringResource(R.string.app_settings_battery_optimization_exempted),
|
||||
summaryOff = stringResource(R.string.app_settings_battery_optimization_optimized)
|
||||
) {
|
||||
if (batterySavingExempted)
|
||||
onBatterySavingSettings()
|
||||
else
|
||||
onExemptFromBatterySaving()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AppSettings_Debugging_Preview() {
|
||||
Column {
|
||||
AppSettings_Debugging(
|
||||
verboseLogging = false,
|
||||
onUpdateVerboseLogging = {},
|
||||
batterySavingExempted = true,
|
||||
onExemptFromBatterySaving = {},
|
||||
onBatterySavingSettings = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Connection(
|
||||
proxyType: Int,
|
||||
onProxyTypeUpdated: (Int) -> Unit = {},
|
||||
proxyHostName: String? = null,
|
||||
onProxyHostNameUpdated: (String) -> Unit = {},
|
||||
proxyPort: Int? = null,
|
||||
onProxyPortUpdated: (Int) -> Unit = {}
|
||||
) {
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_connection))
|
||||
}
|
||||
|
||||
val proxyTypeNames = stringArrayResource(R.array.app_settings_proxy_types)
|
||||
val proxyTypeValues = stringArrayResource(R.array.app_settings_proxy_type_values).map { it.toInt() }
|
||||
var showProxyTypeInputDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_proxy),
|
||||
summary = proxyTypeNames[proxyTypeValues.indexOf(proxyType)]
|
||||
) {
|
||||
showProxyTypeInputDialog = true
|
||||
}
|
||||
if (showProxyTypeInputDialog)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(R.string.app_settings_proxy),
|
||||
namesAndValues = proxyTypeNames.zip(proxyTypeValues.map { it.toString() }),
|
||||
initialValue = proxyType.toString(),
|
||||
onValueSelected = { newValue ->
|
||||
onProxyTypeUpdated(newValue.toInt())
|
||||
AppSettingsScreen(
|
||||
onNavDebugInfo = {
|
||||
startActivity(Intent(this, DebugInfoActivity::class.java))
|
||||
},
|
||||
onDismiss = { showProxyTypeInputDialog = false }
|
||||
)
|
||||
|
||||
if (proxyType !in listOf(Settings.PROXY_TYPE_SYSTEM, Settings.PROXY_TYPE_NONE)) {
|
||||
var showProxyHostNameInputDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_proxy_host),
|
||||
summary = proxyHostName
|
||||
) {
|
||||
showProxyHostNameInputDialog = true
|
||||
}
|
||||
if (showProxyHostNameInputDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.app_settings_proxy_host),
|
||||
initialValue = proxyHostName,
|
||||
keyboardType = KeyboardType.Uri,
|
||||
onValueEntered = onProxyHostNameUpdated,
|
||||
onDismiss = { showProxyHostNameInputDialog = false }
|
||||
)
|
||||
|
||||
var showProxyPortInputDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_proxy_port),
|
||||
summary = proxyPort?.toString()
|
||||
) {
|
||||
showProxyPortInputDialog = true
|
||||
}
|
||||
if (showProxyPortInputDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.app_settings_proxy_port),
|
||||
initialValue = proxyPort?.toString(),
|
||||
keyboardType = KeyboardType.Number,
|
||||
onValueEntered = {
|
||||
try {
|
||||
val newPort = it.toInt()
|
||||
if (newPort in 1..65535)
|
||||
onProxyPortUpdated(newPort)
|
||||
} catch (_: NumberFormatException) {
|
||||
// user entered invalid port number
|
||||
}
|
||||
},
|
||||
onDismiss = { showProxyPortInputDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AppSettings_Connection_Preview() {
|
||||
Column {
|
||||
AppSettings_Connection(
|
||||
proxyType = Settings.PROXY_TYPE_HTTP
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Security(
|
||||
distrustSystemCerts: Boolean,
|
||||
onDistrustSystemCertsUpdated: (Boolean) -> Unit = {},
|
||||
onResetCertificates: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_security))
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
checked = distrustSystemCerts,
|
||||
name = stringResource(R.string.app_settings_distrust_system_certs),
|
||||
summaryOn = stringResource(R.string.app_settings_distrust_system_certs_on),
|
||||
summaryOff = stringResource(R.string.app_settings_distrust_system_certs_off)
|
||||
) {
|
||||
onDistrustSystemCertsUpdated(it)
|
||||
}
|
||||
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_reset_certificates),
|
||||
summary = stringResource(R.string.app_settings_reset_certificates_summary),
|
||||
onClick = onResetCertificates
|
||||
)
|
||||
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_security_app_permissions),
|
||||
summary = stringResource(R.string.app_settings_security_app_permissions_summary),
|
||||
onClick = {
|
||||
context.startActivity(Intent(context, PermissionsActivity::class.java))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AppSettings_Security_Preview() {
|
||||
Column {
|
||||
AppSettings_Security(
|
||||
distrustSystemCerts = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_UserInterface(
|
||||
theme: Int,
|
||||
onThemeSelected: (Int) -> Unit = {},
|
||||
onResetHints: () -> Unit = {}
|
||||
) {
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_user_interface))
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26)
|
||||
Setting(
|
||||
icon = Icons.Default.Notifications,
|
||||
name = stringResource(R.string.app_settings_notification_settings),
|
||||
summary = stringResource(R.string.app_settings_notification_settings_summary)
|
||||
) {
|
||||
val intent = Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
val themeNames = stringArrayResource(R.array.app_settings_theme_names)
|
||||
val themeValues = stringArrayResource(R.array.app_settings_theme_values).map { it.toInt() }
|
||||
var showThemeDialog by remember { mutableStateOf(false) }
|
||||
val themeValueIdx = themeValues.indexOf(theme).takeIf { it != -1 }
|
||||
Setting(
|
||||
icon = Icons.Default.InvertColors,
|
||||
name = stringResource(R.string.app_settings_theme_title),
|
||||
summary = themeValueIdx?.let { themeNames[it] }
|
||||
) {
|
||||
showThemeDialog = true
|
||||
}
|
||||
if (showThemeDialog)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(R.string.app_settings_theme_title),
|
||||
namesAndValues = themeNames.zip(themeValues.map { it.toString() }),
|
||||
initialValue = theme.toString(),
|
||||
onValueSelected = {
|
||||
onThemeSelected(it.toInt())
|
||||
onExemptFromBatterySaving = {
|
||||
startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
|
||||
)
|
||||
)
|
||||
},
|
||||
onDismiss = { showThemeDialog = false }
|
||||
)
|
||||
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_reset_hints),
|
||||
summary = stringResource(R.string.app_settings_reset_hints_summary),
|
||||
onClick = onResetHints
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AppSettings_UserInterface_Preview() {
|
||||
Column {
|
||||
AppSettings_UserInterface(
|
||||
theme = Settings.PREFERRED_THEME_DEFAULT
|
||||
onBatterySavingSettings = {
|
||||
startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
|
||||
},
|
||||
onNavTasksScreen = {
|
||||
startActivity(Intent(this, TasksActivity::class.java))
|
||||
},
|
||||
onShowNotificationSettings = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
|
||||
}
|
||||
)
|
||||
},
|
||||
onNavPermissionsScreen = {
|
||||
startActivity(Intent(this, PermissionsActivity::class.java))
|
||||
},
|
||||
onNavUp = ::onSupportNavigateUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Integration(
|
||||
taskProvider: TaskProvider.ProviderName? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_integration))
|
||||
}
|
||||
|
||||
val pm = context.packageManager
|
||||
val appInfo = taskProvider?.packageName?.let { pkgName ->
|
||||
pm.getApplicationInfo(pkgName, 0)
|
||||
}
|
||||
val appName = appInfo?.loadLabel(pm)?.toString()
|
||||
Setting(
|
||||
name = {
|
||||
Text(stringResource(R.string.app_settings_tasks_provider))
|
||||
},
|
||||
icon = {
|
||||
if (appInfo != null) {
|
||||
val icon = appInfo.loadIcon(pm)
|
||||
Image(icon.toBitmap().asImageBitmap(), appName)
|
||||
}
|
||||
},
|
||||
summary = appName ?: stringResource(R.string.app_settings_tasks_provider_none)
|
||||
) {
|
||||
context.startActivity(Intent(context, TasksActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AppSettings_Integration_Preview() {
|
||||
Column {
|
||||
AppSettings_Integration()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
val context: Application,
|
||||
val settings: SettingsManager
|
||||
) : ViewModel() {
|
||||
|
||||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
private val powerManager = context.getSystemService<PowerManager>()!!
|
||||
val batterySavingExempted = broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED), immediate = true)
|
||||
.map { powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
fun getPrefBoolean(keyToObserve: String): LiveData<Boolean?> =
|
||||
object : LiveData<Boolean?>(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
override fun onActive() {
|
||||
preferences.registerOnSharedPreferenceChangeListener(this)
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
preferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
if (preferences.contains(keyToObserve))
|
||||
postValue(preferences.getBoolean(keyToObserve, false))
|
||||
else
|
||||
postValue(null)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
if (key == keyToObserve)
|
||||
update()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun putPrefBoolean(key: String, value: Boolean) {
|
||||
preferences
|
||||
.edit()
|
||||
.putBoolean(key, value)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun resetCertificates() {
|
||||
CustomCertStore.getInstance(context).clearUserDecisions()
|
||||
}
|
||||
|
||||
fun resetHints() {
|
||||
settings.remove(BatteryOptimizationsPageModel.HINT_BATTERY_OPTIMIZATIONS)
|
||||
settings.remove(BatteryOptimizationsPageModel.HINT_AUTOSTART_PERMISSION)
|
||||
settings.remove(TasksModel.HINT_OPENTASKS_NOT_INSTALLED)
|
||||
settings.remove(OpenSourcePage.Model.SETTING_NEXT_DONATION_POPUP)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
104
app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt
Normal file
104
app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt
Normal file
|
@ -0,0 +1,104 @@
|
|||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.cert4android.CustomCertStore
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.repository.PreferenceRepository
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPageModel
|
||||
import at.bitfire.davdroid.ui.intro.OpenSourcePage
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.util.broadcastReceiverFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AppSettingsModel @Inject constructor(
|
||||
val context: Application,
|
||||
private val preference: PreferenceRepository,
|
||||
private val settings: SettingsManager
|
||||
) : ViewModel() {
|
||||
|
||||
// debugging
|
||||
|
||||
private val powerManager = context.getSystemService<PowerManager>()!!
|
||||
val batterySavingExempted = broadcastReceiverFlow(context, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED), immediate = true)
|
||||
.map { powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
fun verboseLogging() = preference.logToFileFlow()
|
||||
fun updateVerboseLogging(verbose: Boolean) {
|
||||
preference.logToFile(verbose)
|
||||
}
|
||||
|
||||
|
||||
// connection
|
||||
|
||||
fun proxyType() = settings.getIntFlow(Settings.PROXY_TYPE)
|
||||
fun updateProxyType(type: Int) {
|
||||
settings.putInt(Settings.PROXY_TYPE, type)
|
||||
}
|
||||
|
||||
fun proxyHostName() = settings.getStringFlow(Settings.PROXY_HOST)
|
||||
fun updateProxyHostName(host: String) {
|
||||
settings.putString(Settings.PROXY_HOST, host)
|
||||
}
|
||||
|
||||
fun proxyPort() = settings.getIntFlow(Settings.PROXY_PORT)
|
||||
fun updateProxyPort(port: Int) {
|
||||
settings.putInt(Settings.PROXY_PORT, port)
|
||||
}
|
||||
|
||||
|
||||
// security
|
||||
|
||||
fun distrustSystemCertificates() = settings.getBooleanFlow(Settings.DISTRUST_SYSTEM_CERTIFICATES)
|
||||
fun updateDistrustSystemCertificates(distrust: Boolean) {
|
||||
settings.putBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES, distrust)
|
||||
}
|
||||
|
||||
fun resetCertificates() {
|
||||
CustomCertStore.getInstance(context).clearUserDecisions()
|
||||
}
|
||||
|
||||
|
||||
// user interface
|
||||
|
||||
fun theme() = settings.getIntFlow(Settings.PREFERRED_THEME)
|
||||
fun updateTheme(theme: Int) {
|
||||
settings.putInt(Settings.PREFERRED_THEME, theme)
|
||||
UiUtils.updateTheme(context)
|
||||
}
|
||||
|
||||
fun resetHints() {
|
||||
settings.remove(BatteryOptimizationsPageModel.HINT_BATTERY_OPTIMIZATIONS)
|
||||
settings.remove(BatteryOptimizationsPageModel.HINT_AUTOSTART_PERMISSION)
|
||||
settings.remove(TasksModel.HINT_OPENTASKS_NOT_INSTALLED)
|
||||
settings.remove(OpenSourcePage.Model.SETTING_NEXT_DONATION_POPUP)
|
||||
}
|
||||
|
||||
|
||||
// tasks
|
||||
|
||||
val pm: PackageManager = context.packageManager
|
||||
private val appInfoFlow = TaskUtils.currentProviderFlow(context, viewModelScope).map { tasksProvider ->
|
||||
tasksProvider?.packageName?.let { pkgName ->
|
||||
pm.getApplicationInfo(pkgName, 0)
|
||||
}
|
||||
}
|
||||
val appName = appInfoFlow.map { it?.loadLabel(pm)?.toString() }
|
||||
val icon = appInfoFlow.map { it?.loadIcon(pm) }
|
||||
|
||||
|
||||
}
|
485
app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt
Normal file
485
app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt
Normal file
|
@ -0,0 +1,485 @@
|
|||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.Image
|
||||
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.Adb
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.InvertColors
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.SyncProblem
|
||||
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.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.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AppSettingsScreen(
|
||||
onNavDebugInfo: () -> Unit,
|
||||
onExemptFromBatterySaving: () -> Unit,
|
||||
onBatterySavingSettings: () -> Unit,
|
||||
onNavPermissionsScreen: () -> Unit,
|
||||
onShowNotificationSettings: () -> Unit,
|
||||
onNavTasksScreen: () -> Unit,
|
||||
onNavUp: () -> Unit,
|
||||
model: AppSettingsModel = viewModel()
|
||||
) {
|
||||
AppTheme {
|
||||
AppSettingsScreen(
|
||||
onNavDebugInfo = onNavDebugInfo,
|
||||
verboseLogging = model.verboseLogging().collectAsStateWithLifecycle(false).value,
|
||||
onUpdateVerboseLogging = model::updateVerboseLogging,
|
||||
batterySavingExempted = model.batterySavingExempted.collectAsStateWithLifecycle().value,
|
||||
onExemptFromBatterySaving = onExemptFromBatterySaving,
|
||||
onBatterySavingSettings = onBatterySavingSettings,
|
||||
onNavUp = onNavUp,
|
||||
|
||||
// Connection
|
||||
proxyType = model.proxyType().collectAsStateWithLifecycle(null).value ?: Settings.PROXY_TYPE_NONE,
|
||||
onProxyTypeUpdated = model::updateProxyType,
|
||||
proxyHostName = model.proxyHostName().collectAsStateWithLifecycle(null).value,
|
||||
onProxyHostNameUpdated = model::updateProxyHostName,
|
||||
proxyPort = model.proxyPort().collectAsStateWithLifecycle(null).value,
|
||||
onProxyPortUpdated = model::updateProxyPort,
|
||||
|
||||
// Security
|
||||
distrustSystemCerts = model.distrustSystemCertificates().collectAsStateWithLifecycle(null).value ?: false,
|
||||
onDistrustSystemCertsUpdated = model::updateDistrustSystemCertificates,
|
||||
onResetCertificates = model::resetCertificates,
|
||||
onNavPermissionsScreen = onNavPermissionsScreen,
|
||||
|
||||
// User interface
|
||||
onShowNotificationSettings = onShowNotificationSettings,
|
||||
theme = model.theme().collectAsStateWithLifecycle(null).value ?: Settings.PREFERRED_THEME_DEFAULT,
|
||||
onThemeSelected = model::updateTheme,
|
||||
onResetHints = model::resetHints,
|
||||
|
||||
// Integration (Tasks)
|
||||
tasksAppName = model.appName.collectAsStateWithLifecycle(null).value ?: stringResource(R.string.app_settings_tasks_provider_none),
|
||||
tasksAppIcon = model.icon.collectAsStateWithLifecycle(null).value,
|
||||
onNavTasksScreen = onNavTasksScreen
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("BatteryLife")
|
||||
@Composable
|
||||
fun AppSettingsScreen(
|
||||
onNavDebugInfo: () -> Unit,
|
||||
verboseLogging: Boolean,
|
||||
onUpdateVerboseLogging: (Boolean) -> Unit,
|
||||
batterySavingExempted: Boolean,
|
||||
onExemptFromBatterySaving: () -> Unit,
|
||||
onBatterySavingSettings: () -> Unit,
|
||||
|
||||
// AppSettings connection
|
||||
proxyType: Int,
|
||||
onProxyTypeUpdated: (Int) -> Unit,
|
||||
proxyHostName: String?,
|
||||
onProxyHostNameUpdated: (String) -> Unit,
|
||||
proxyPort: Int?,
|
||||
onProxyPortUpdated: (Int) -> Unit,
|
||||
|
||||
// AppSettings security
|
||||
distrustSystemCerts: Boolean,
|
||||
onDistrustSystemCertsUpdated: (Boolean) -> Unit,
|
||||
onResetCertificates: () -> Unit,
|
||||
onNavPermissionsScreen: () -> Unit,
|
||||
|
||||
// AppSettings UserInterface
|
||||
theme: Int,
|
||||
onThemeSelected: (Int) -> Unit,
|
||||
onResetHints: () -> Unit,
|
||||
|
||||
// AppSettings Integration
|
||||
tasksAppName: String,
|
||||
tasksAppIcon: Drawable?,
|
||||
onNavTasksScreen: () -> Unit,
|
||||
|
||||
onShowNotificationSettings: () -> Unit,
|
||||
onNavUp: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavUp) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.app_settings)) },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
val settingsUri = Constants.MANUAL_URL.buildUpon()
|
||||
.appendPath(Constants.MANUAL_PATH_SETTINGS)
|
||||
.fragment(Constants.MANUAL_FRAGMENT_APP_SETTINGS)
|
||||
.build()
|
||||
uriHandler.openUri(settingsUri.toString())
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.help))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
AppSettings_Debugging(
|
||||
onNavDebugInfo = onNavDebugInfo,
|
||||
verboseLogging = verboseLogging,
|
||||
onUpdateVerboseLogging = onUpdateVerboseLogging,
|
||||
batterySavingExempted = batterySavingExempted,
|
||||
onExemptFromBatterySaving = onExemptFromBatterySaving,
|
||||
onBatterySavingSettings = onBatterySavingSettings
|
||||
)
|
||||
|
||||
AppSettings_Connection(
|
||||
proxyType = proxyType,
|
||||
onProxyTypeUpdated = onProxyTypeUpdated,
|
||||
proxyHostName = proxyHostName,
|
||||
onProxyHostNameUpdated = onProxyHostNameUpdated,
|
||||
proxyPort = proxyPort,
|
||||
onProxyPortUpdated = onProxyPortUpdated,
|
||||
)
|
||||
|
||||
val resetCertificatesSuccessMessage = stringResource(R.string.app_settings_reset_certificates_success)
|
||||
AppSettings_Security(
|
||||
distrustSystemCerts = distrustSystemCerts,
|
||||
onDistrustSystemCertsUpdated = onDistrustSystemCertsUpdated,
|
||||
onResetCertificates = {
|
||||
onResetCertificates()
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(resetCertificatesSuccessMessage)
|
||||
}
|
||||
},
|
||||
onNavPermissionsScreen = onNavPermissionsScreen
|
||||
)
|
||||
|
||||
val resetHintsSuccessMessage = stringResource(R.string.app_settings_reset_hints_success)
|
||||
AppSettings_UserInterface(
|
||||
theme = theme,
|
||||
onThemeSelected = onThemeSelected,
|
||||
onResetHints = {
|
||||
onResetHints()
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(resetHintsSuccessMessage)
|
||||
}
|
||||
},
|
||||
onShowNotificationSettings = onShowNotificationSettings
|
||||
)
|
||||
|
||||
AppSettings_Integration(
|
||||
appName = tasksAppName,
|
||||
icon = tasksAppIcon,
|
||||
onNavTasksScreen = onNavTasksScreen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun AppSettingsScreen_Preview() {
|
||||
AppTheme {
|
||||
AppSettingsScreen(
|
||||
onNavDebugInfo = {},
|
||||
verboseLogging = true,
|
||||
batterySavingExempted = true,
|
||||
proxyType = 0,
|
||||
proxyHostName = "true",
|
||||
proxyPort = 0,
|
||||
distrustSystemCerts = true,
|
||||
theme = 0,
|
||||
onUpdateVerboseLogging = {},
|
||||
onProxyHostNameUpdated = {},
|
||||
onExemptFromBatterySaving = {},
|
||||
onBatterySavingSettings = {},
|
||||
onShowNotificationSettings = {},
|
||||
onNavUp = {},
|
||||
onProxyTypeUpdated = {},
|
||||
onProxyPortUpdated = {},
|
||||
onDistrustSystemCertsUpdated = {},
|
||||
onResetCertificates = {},
|
||||
onNavPermissionsScreen = {},
|
||||
onThemeSelected = {},
|
||||
onResetHints = {},
|
||||
tasksAppName = "No tasks app",
|
||||
tasksAppIcon = null,
|
||||
onNavTasksScreen = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Debugging(
|
||||
onNavDebugInfo: () -> Unit,
|
||||
verboseLogging: Boolean,
|
||||
onUpdateVerboseLogging: (Boolean) -> Unit,
|
||||
batterySavingExempted: Boolean,
|
||||
onExemptFromBatterySaving: () -> Unit,
|
||||
onBatterySavingSettings: () -> Unit
|
||||
) {
|
||||
SettingsHeader {
|
||||
Text(stringResource(R.string.app_settings_debug))
|
||||
}
|
||||
|
||||
Setting(
|
||||
icon = Icons.Default.BugReport,
|
||||
name = stringResource(R.string.app_settings_show_debug_info),
|
||||
summary = stringResource(R.string.app_settings_show_debug_info_details)
|
||||
) {
|
||||
onNavDebugInfo()
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
icon = Icons.Default.Adb,
|
||||
checked = verboseLogging,
|
||||
name = stringResource(R.string.app_settings_logging),
|
||||
summaryOn = stringResource(R.string.app_settings_logging_on),
|
||||
summaryOff = stringResource(R.string.app_settings_logging_off)
|
||||
) {
|
||||
onUpdateVerboseLogging(it)
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
checked = batterySavingExempted,
|
||||
icon = Icons.Default.SyncProblem.takeUnless { batterySavingExempted },
|
||||
name = stringResource(R.string.app_settings_battery_optimization),
|
||||
summaryOn = stringResource(R.string.app_settings_battery_optimization_exempted),
|
||||
summaryOff = stringResource(R.string.app_settings_battery_optimization_optimized)
|
||||
) {
|
||||
if (batterySavingExempted)
|
||||
onBatterySavingSettings()
|
||||
else
|
||||
onExemptFromBatterySaving()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Connection(
|
||||
proxyType: Int,
|
||||
onProxyTypeUpdated: (Int) -> Unit = {},
|
||||
proxyHostName: String? = null,
|
||||
onProxyHostNameUpdated: (String) -> Unit = {},
|
||||
proxyPort: Int? = null,
|
||||
onProxyPortUpdated: (Int) -> Unit = {}
|
||||
) {
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_connection))
|
||||
}
|
||||
|
||||
val proxyTypeNames = stringArrayResource(R.array.app_settings_proxy_types)
|
||||
val proxyTypeValues = stringArrayResource(R.array.app_settings_proxy_type_values).map { it.toInt() }
|
||||
var showProxyTypeInputDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_proxy),
|
||||
summary = proxyTypeNames[proxyTypeValues.indexOf(proxyType)]
|
||||
) {
|
||||
showProxyTypeInputDialog = true
|
||||
}
|
||||
if (showProxyTypeInputDialog)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(R.string.app_settings_proxy),
|
||||
namesAndValues = proxyTypeNames.zip(proxyTypeValues.map { it.toString() }),
|
||||
initialValue = proxyType.toString(),
|
||||
onValueSelected = { newValue ->
|
||||
onProxyTypeUpdated(newValue.toInt())
|
||||
},
|
||||
onDismiss = { showProxyTypeInputDialog = false }
|
||||
)
|
||||
|
||||
if (proxyType !in listOf(Settings.PROXY_TYPE_SYSTEM, Settings.PROXY_TYPE_NONE)) {
|
||||
var showProxyHostNameInputDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_proxy_host),
|
||||
summary = proxyHostName
|
||||
) {
|
||||
showProxyHostNameInputDialog = true
|
||||
}
|
||||
if (showProxyHostNameInputDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.app_settings_proxy_host),
|
||||
initialValue = proxyHostName,
|
||||
keyboardType = KeyboardType.Uri,
|
||||
onValueEntered = onProxyHostNameUpdated,
|
||||
onDismiss = { showProxyHostNameInputDialog = false }
|
||||
)
|
||||
|
||||
var showProxyPortInputDialog by remember { mutableStateOf(false) }
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_proxy_port),
|
||||
summary = proxyPort?.toString()
|
||||
) {
|
||||
showProxyPortInputDialog = true
|
||||
}
|
||||
if (showProxyPortInputDialog)
|
||||
EditTextInputDialog(
|
||||
title = stringResource(R.string.app_settings_proxy_port),
|
||||
initialValue = proxyPort?.toString(),
|
||||
keyboardType = KeyboardType.Number,
|
||||
onValueEntered = {
|
||||
try {
|
||||
val newPort = it.toInt()
|
||||
if (newPort in 1..65535)
|
||||
onProxyPortUpdated(newPort)
|
||||
} catch (_: NumberFormatException) {
|
||||
// user entered invalid port number
|
||||
}
|
||||
},
|
||||
onDismiss = { showProxyPortInputDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Security(
|
||||
distrustSystemCerts: Boolean,
|
||||
onDistrustSystemCertsUpdated: (Boolean) -> Unit,
|
||||
onResetCertificates: () -> Unit,
|
||||
onNavPermissionsScreen: () -> Unit
|
||||
) {
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_security))
|
||||
}
|
||||
|
||||
SwitchSetting(
|
||||
checked = distrustSystemCerts,
|
||||
name = stringResource(R.string.app_settings_distrust_system_certs),
|
||||
summaryOn = stringResource(R.string.app_settings_distrust_system_certs_on),
|
||||
summaryOff = stringResource(R.string.app_settings_distrust_system_certs_off)
|
||||
) {
|
||||
onDistrustSystemCertsUpdated(it)
|
||||
}
|
||||
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_reset_certificates),
|
||||
summary = stringResource(R.string.app_settings_reset_certificates_summary),
|
||||
onClick = onResetCertificates
|
||||
)
|
||||
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_security_app_permissions),
|
||||
summary = stringResource(R.string.app_settings_security_app_permissions_summary),
|
||||
onClick = onNavPermissionsScreen
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_UserInterface(
|
||||
theme: Int,
|
||||
onThemeSelected: (Int) -> Unit = {},
|
||||
onResetHints: () -> Unit = {},
|
||||
onShowNotificationSettings: () -> Unit = {}
|
||||
) {
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_user_interface))
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26)
|
||||
Setting(
|
||||
icon = Icons.Default.Notifications,
|
||||
name = stringResource(R.string.app_settings_notification_settings),
|
||||
summary = stringResource(R.string.app_settings_notification_settings_summary),
|
||||
onClick = onShowNotificationSettings
|
||||
)
|
||||
|
||||
val themeNames = stringArrayResource(R.array.app_settings_theme_names)
|
||||
val themeValues = stringArrayResource(R.array.app_settings_theme_values).map { it.toInt() }
|
||||
var showThemeDialog by remember { mutableStateOf(false) }
|
||||
val themeValueIdx = themeValues.indexOf(theme).takeIf { it != -1 }
|
||||
Setting(
|
||||
icon = Icons.Default.InvertColors,
|
||||
name = stringResource(R.string.app_settings_theme_title),
|
||||
summary = themeValueIdx?.let { themeNames[it] }
|
||||
) {
|
||||
showThemeDialog = true
|
||||
}
|
||||
if (showThemeDialog)
|
||||
MultipleChoiceInputDialog(
|
||||
title = stringResource(R.string.app_settings_theme_title),
|
||||
namesAndValues = themeNames.zip(themeValues.map { it.toString() }),
|
||||
initialValue = theme.toString(),
|
||||
onValueSelected = {
|
||||
onThemeSelected(it.toInt())
|
||||
},
|
||||
onDismiss = { showThemeDialog = false }
|
||||
)
|
||||
|
||||
Setting(
|
||||
name = stringResource(R.string.app_settings_reset_hints),
|
||||
summary = stringResource(R.string.app_settings_reset_hints_summary),
|
||||
onClick = onResetHints
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppSettings_Integration(
|
||||
appName: String,
|
||||
icon: Drawable? = null,
|
||||
onNavTasksScreen: () -> Unit = {}
|
||||
) {
|
||||
SettingsHeader(divider = true) {
|
||||
Text(stringResource(R.string.app_settings_integration))
|
||||
}
|
||||
Setting(
|
||||
name = {
|
||||
Text(stringResource(R.string.app_settings_tasks_provider))
|
||||
},
|
||||
icon = {
|
||||
icon?.let {
|
||||
Image(icon.toBitmap().asImageBitmap(), appName)
|
||||
}
|
||||
},
|
||||
summary = appName,
|
||||
onClick = onNavTasksScreen
|
||||
)
|
||||
}
|
|
@ -6,20 +6,23 @@ package at.bitfire.davdroid.ui.composable
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import at.bitfire.davdroid.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Deprecated("Directly use TopAppBar instead.", replaceWith = ReplaceWith("TopAppBar"))
|
||||
fun BasicTopAppBar(
|
||||
@StringRes titleStringRes: Int,
|
||||
actions: @Composable () -> Unit = {},
|
||||
onNavigateUp: () -> Unit
|
||||
) {
|
||||
TopAppBar(
|
||||
|
@ -32,12 +35,4 @@ fun BasicTopAppBar(
|
|||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppCompatActivity.BasicTopAppBar(@StringRes titleStringRes: Int) {
|
||||
BasicTopAppBar(
|
||||
titleStringRes = titleStringRes,
|
||||
onNavigateUp = ::onSupportNavigateUp
|
||||
)
|
||||
}
|
|
@ -12,13 +12,13 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -59,7 +59,7 @@ fun EditTextInputDialog(
|
|||
title = {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.body1
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
},
|
||||
text = {
|
||||
|
@ -148,7 +148,7 @@ fun MultipleChoiceInputDialog(
|
|||
Column {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.body1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
|
@ -173,7 +173,7 @@ fun MultipleChoiceInputDialog(
|
|||
)
|
||||
Text(
|
||||
name,
|
||||
style = MaterialTheme.typography.body1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
|
|
|
@ -11,15 +11,15 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
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
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
|
@ -41,8 +41,8 @@ fun SettingsHeader(divider: Boolean = false, content: @Composable () -> Unit) {
|
|||
.fillMaxWidth()
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.body1.copy(
|
||||
color = MaterialTheme.colors.secondary
|
||||
LocalTextStyle provides MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
content()
|
||||
|
@ -94,12 +94,12 @@ fun Setting(
|
|||
.padding(start = 8.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
ProvideTextStyle(MaterialTheme.typography.body1) {
|
||||
ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
|
||||
name()
|
||||
}
|
||||
|
||||
if (summary != null)
|
||||
Text(summary, style = MaterialTheme.typography.body2)
|
||||
Text(summary, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
end()
|
||||
|
@ -120,7 +120,7 @@ fun Setting(
|
|||
Icon(icon, contentDescription = name)
|
||||
},
|
||||
name = {
|
||||
Text(name, style = MaterialTheme.typography.body1)
|
||||
Text(name, style = MaterialTheme.typography.bodyLarge)
|
||||
},
|
||||
summary = summary,
|
||||
enabled = enabled,
|
||||
|
|
Loading…
Reference in a new issue