From 6f0266983296c8e655445a6d20d64ff8d8e4b3e1 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Thu, 16 May 2024 13:00:20 +0200 Subject: [PATCH] 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 --- .../kotlin/at/bitfire/davdroid/Constants.kt | 2 + .../repository/PreferenceRepository.kt | 59 ++ .../davdroid/ui/AppSettingsActivity.kt | 550 +----------------- .../bitfire/davdroid/ui/AppSettingsModel.kt | 104 ++++ .../bitfire/davdroid/ui/AppSettingsScreen.kt | 485 +++++++++++++++ .../davdroid/ui/composable/BasicTopAppBar.kt | 19 +- .../davdroid/ui/composable/InputDialogs.kt | 20 +- .../davdroid/ui/composable/Settings.kt | 24 +- 8 files changed, 709 insertions(+), 554 deletions(-) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt diff --git a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt index 6d0d208a..d885f50f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt @@ -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() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt new file mode 100644 index 00000000..be996372 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/PreferenceRepository.kt @@ -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 = observeAsFlow(Logger.LOG_TO_FILE) { + preferences.getBoolean(Logger.LOG_TO_FILE, false) + } + + + private fun observeAsFlow(keyToObserve: String, getValue: () -> T): Flow = + callbackFlow { + val listener = OnSharedPreferenceChangeListener { _, key -> + if (key == keyToObserve) { + trySend(getValue()) + } + } + preferences.registerOnSharedPreferenceChangeListener(listener) + + awaitClose { + preferences.unregisterOnSharedPreferenceChangeListener(listener) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt index 1fdf54ff..8cddc563 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -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()!! - 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 = - object : LiveData(), 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) - } - - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt new file mode 100644 index 00000000..2d143fca --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsModel.kt @@ -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()!! + 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) } + + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt new file mode 100644 index 00000000..e66f6fff --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt index 916c4312..15e5649a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/BasicTopAppBar.kt @@ -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 - ) } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt index b708b40a..b98f7941 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/InputDialogs.kt @@ -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 { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt index 0e943850..b0a6b966 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/composable/Settings.kt @@ -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,