mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-21 18:51:52 +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()
|
val MANUAL_URL = "https://manual.davx5.com".toUri()
|
||||||
const val MANUAL_PATH_ACCOUNTS_COLLECTIONS = "accounts_collections.html"
|
const val MANUAL_PATH_ACCOUNTS_COLLECTIONS = "accounts_collections.html"
|
||||||
const val MANUAL_FRAGMENT_SERVICE_DISCOVERY = "how-does-service-discovery-work"
|
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"
|
const val MANUAL_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
|
||||||
|
|
||||||
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()
|
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
|
package at.bitfire.davdroid.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.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.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
|
@AndroidEntryPoint
|
||||||
class AppSettingsActivity: AppCompatActivity() {
|
class AppSettingsActivity: AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
@SuppressLint("BatteryLife")
|
||||||
const val APP_SETTINGS_HELP_URL = "https://manual.davx5.com/settings.html#app-wide-settings"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
M2Theme {
|
AppSettingsScreen(
|
||||||
AppSettings()
|
onNavDebugInfo = {
|
||||||
}
|
startActivity(Intent(this, DebugInfoActivity::class.java))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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())
|
|
||||||
},
|
},
|
||||||
onDismiss = { showProxyTypeInputDialog = false }
|
onExemptFromBatterySaving = {
|
||||||
)
|
startActivity(
|
||||||
|
Intent(
|
||||||
if (proxyType !in listOf(Settings.PROXY_TYPE_SYSTEM, Settings.PROXY_TYPE_NONE)) {
|
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||||
var showProxyHostNameInputDialog by remember { mutableStateOf(false) }
|
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
|
||||||
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())
|
|
||||||
},
|
},
|
||||||
onDismiss = { showThemeDialog = false }
|
onBatterySavingSettings = {
|
||||||
)
|
startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
|
||||||
|
},
|
||||||
Setting(
|
onNavTasksScreen = {
|
||||||
name = stringResource(R.string.app_settings_reset_hints),
|
startActivity(Intent(this, TasksActivity::class.java))
|
||||||
summary = stringResource(R.string.app_settings_reset_hints_summary),
|
},
|
||||||
onClick = onResetHints
|
onShowNotificationSettings = {
|
||||||
)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
}
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||||
@Composable
|
putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
|
||||||
@Preview
|
}
|
||||||
fun AppSettings_UserInterface_Preview() {
|
)
|
||||||
Column {
|
},
|
||||||
AppSettings_UserInterface(
|
onNavPermissionsScreen = {
|
||||||
theme = Settings.PREFERRED_THEME_DEFAULT
|
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.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Deprecated("Directly use TopAppBar instead.", replaceWith = ReplaceWith("TopAppBar"))
|
@Deprecated("Directly use TopAppBar instead.", replaceWith = ReplaceWith("TopAppBar"))
|
||||||
fun BasicTopAppBar(
|
fun BasicTopAppBar(
|
||||||
@StringRes titleStringRes: Int,
|
@StringRes titleStringRes: Int,
|
||||||
|
actions: @Composable () -> Unit = {},
|
||||||
onNavigateUp: () -> Unit
|
onNavigateUp: () -> Unit
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
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.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -59,7 +59,7 @@ fun EditTextInputDialog(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style = MaterialTheme.typography.body1
|
style = MaterialTheme.typography.bodyLarge
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
|
@ -148,7 +148,7 @@ fun MultipleChoiceInputDialog(
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
|
@ -173,7 +173,7 @@ fun MultipleChoiceInputDialog(
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
name,
|
name,
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clickable {
|
.clickable {
|
||||||
|
|
|
@ -11,15 +11,15 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Folder
|
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.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
@ -41,8 +41,8 @@ fun SettingsHeader(divider: Boolean = false, content: @Composable () -> Unit) {
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides MaterialTheme.typography.body1.copy(
|
LocalTextStyle provides MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = MaterialTheme.colors.secondary
|
color = MaterialTheme.colorScheme.secondary
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
|
@ -94,12 +94,12 @@ fun Setting(
|
||||||
.padding(start = 8.dp)
|
.padding(start = 8.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(MaterialTheme.typography.body1) {
|
ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
|
||||||
name()
|
name()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (summary != null)
|
if (summary != null)
|
||||||
Text(summary, style = MaterialTheme.typography.body2)
|
Text(summary, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
|
|
||||||
end()
|
end()
|
||||||
|
@ -120,7 +120,7 @@ fun Setting(
|
||||||
Icon(icon, contentDescription = name)
|
Icon(icon, contentDescription = name)
|
||||||
},
|
},
|
||||||
name = {
|
name = {
|
||||||
Text(name, style = MaterialTheme.typography.body1)
|
Text(name, style = MaterialTheme.typography.bodyLarge)
|
||||||
},
|
},
|
||||||
summary = summary,
|
summary = summary,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|
Loading…
Reference in a new issue