Rewrite app settings to Compose (#628)

* [WIP] Rewrite app settings to Compose

* Optical changes

* Add Help button

* Fix URL, preferences LiveData: handle null value

* Fix tests
This commit is contained in:
Ricki Hirner 2024-03-10 19:20:11 +01:00 committed by GitHub
parent 66f0075cc9
commit c8a0128842
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 951 additions and 464 deletions

View file

@ -194,9 +194,7 @@ dependencies {
implementation(libs.bitfire.vcard4android)
// third-party libs
implementation(libs.openid.appauth)
implementation(libs.appintro)
implementation(libs.mikepenz.aboutLibraries)
implementation(libs.commons.collections)
@Suppress("RedundantSuppression")
implementation(libs.commons.io)
@ -205,10 +203,12 @@ dependencies {
@Suppress("RedundantSuppression")
implementation(libs.dnsjava)
implementation(libs.jaredrummler.colorpicker)
implementation(libs.mikepenz.aboutLibraries)
implementation(libs.nsk90.kstatemachine)
implementation(libs.okhttp.base)
implementation(libs.okhttp.brotli)
implementation(libs.okhttp.logging)
implementation(libs.openid.appauth)
// for tests
androidTestImplementation(libs.androidx.arch.core.testing)

View file

@ -4,6 +4,7 @@
package at.bitfire.davdroid.settings
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import at.bitfire.davdroid.TestUtils.getOrAwaitValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
@ -31,6 +32,8 @@ class SettingsManagerTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Inject lateinit var settingsManager: SettingsManager
@ -58,17 +61,15 @@ class SettingsManagerTest {
@Test
fun test_getBooleanLive_getValue() = runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread
fun test_getBooleanLive_getValue() {
val live = settingsManager.getBooleanLive(SETTING_TEST)
assertNull(live.value)
// set value
// posts value to main thread, InstantTaskExecutorRule is required to execute it instantly
settingsManager.putBoolean(SETTING_TEST, true)
assertTrue(live.getOrAwaitValue()!!)
// set another value
live.value = false
assertFalse(live.getOrAwaitValue()!!)
runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread
assertTrue(live.getOrAwaitValue())
}
}

View file

@ -1,13 +0,0 @@
package at.bitfire.davdroid.ui
import org.junit.Assert.assertEquals
import org.junit.Test
class AppSettingsActivityTest {
@Test
fun testResourceQualifierToLanguageTag() {
assertEquals("en", AppSettingsActivity.resourceQualifierToLanguageTag("en"))
assertEquals("en-GB", AppSettingsActivity.resourceQualifierToLanguageTag("en-GB"))
assertEquals("en-GB", AppSettingsActivity.resourceQualifierToLanguageTag("en-rGB"))
}
}

View file

@ -85,6 +85,7 @@
<activity
android:name=".ui.AppSettingsActivity"
android:label="@string/app_settings"
android:theme="@style/AppTheme.NoActionBar"
android:parentActivityName=".ui.AccountsActivity"
android:exported="true">
<intent-filter>

View file

@ -89,7 +89,7 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
NotificationUtils.createChannels(this)
// set light/dark mode
UiUtils.setTheme(this) // when this is called in the asynchronous thread below, it recreates
UiUtils.updateTheme(this) // when this is called in the asynchronous thread below, it recreates
// some current activity and causes an IllegalStateException in rare cases
// don't block UI for some background checks

View file

@ -11,7 +11,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TasksWatcher protected constructor(
class TasksWatcher private constructor(
context: Context
): PackageChangedReceiver(context) {
@ -21,7 +21,6 @@ class TasksWatcher protected constructor(
}
override fun onReceive(context: Context, intent: Intent) {
CoroutineScope(Dispatchers.Default).launch {
updateTaskSync(context)

View file

@ -28,8 +28,8 @@ import java.util.logging.Level
object Logger : SharedPreferences.OnSharedPreferenceChangeListener {
const val LOGGER_NAME = "davx5"
private const val LOG_TO_FILE = "log_to_file"
private const val LOGGER_NAME = "davx5"
const val LOG_TO_FILE = "log_to_file"
val log: java.util.logging.Logger = java.util.logging.Logger.getLogger(LOGGER_NAME)
@ -105,7 +105,6 @@ object Logger : SharedPreferences.OnSharedPreferenceChangeListener {
).build())
val prefIntent = Intent(context, AppSettingsActivity::class.java)
prefIntent.putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, LOG_TO_FILE)
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingPref = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
builder.addAction(NotificationCompat.Action.Builder(

View file

@ -5,6 +5,9 @@
package at.bitfire.davdroid.resource
import android.content.Context
import android.content.pm.PackageManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.syncadapter.SyncUtils
@ -27,13 +30,27 @@ object TaskUtils {
fun currentProvider(context: Context): ProviderName? {
val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager()
val preferredAuthority = settingsManager.getString(Settings.PREFERRED_TASKS_PROVIDER)
ProviderName.entries.toTypedArray()
.sortedByDescending { it.authority == preferredAuthority }
.forEach { providerName ->
if (context.packageManager.resolveContentProvider(providerName.authority, 0) != null)
return providerName
val preferredAuthority = settingsManager.getString(Settings.PREFERRED_TASKS_PROVIDER) ?: return null
return preferredAuthorityToProviderName(preferredAuthority, context.packageManager)
}
fun currentProviderLive(context: Context): LiveData<ProviderName?> {
val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager()
return settingsManager.getStringLive(Settings.PREFERRED_TASKS_PROVIDER).map { preferred ->
preferredAuthorityToProviderName(preferred, context.packageManager)
}
}
private fun preferredAuthorityToProviderName(
preferredAuthority: String,
packageManager: PackageManager
): ProviderName? {
ProviderName.entries.toTypedArray()
.sortedByDescending { it.authority == preferredAuthority }
.forEach { providerName ->
if (packageManager.resolveContentProvider(providerName.authority, 0) != null)
return providerName
}
return null
}

View file

@ -7,9 +7,8 @@ package at.bitfire.davdroid.settings
import android.content.Context
import android.util.NoSuchPropertyException
import androidx.annotation.AnyThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.SettingsManager.OnChangeListener
import dagger.Module
import dagger.Provides
import dagger.hilt.EntryPoint
@ -19,7 +18,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.io.Writer
import java.lang.ref.WeakReference
import java.util.*
import java.util.LinkedList
import java.util.logging.Level
import javax.inject.Singleton
@ -126,14 +125,17 @@ class SettingsManager internal constructor(
fun getBooleanOrNull(key: String): Boolean? = getValue(key) { provider -> provider.getBoolean(key) }
fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key)
fun getBooleanLive(key: String): LiveData<Boolean> = SettingLiveData { getBooleanOrNull(key) }
fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) }
fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key)
fun getIntLive(key: String): LiveData<Int> = SettingLiveData { getIntOrNull(key) }
fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) }
fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key)
fun getString(key: String) = getValue(key) { provider -> provider.getString(key) }
fun getStringLive(key: String): LiveData<String> = SettingLiveData { getString(key) }
fun isWritable(key: String): Boolean {
@ -158,48 +160,41 @@ class SettingsManager internal constructor(
}
fun putBoolean(key: String, value: Boolean?) =
putValue(key, value) { provider -> provider.putBoolean(key, value) }
putValue(key, value) { provider -> provider.putBoolean(key, value) }
fun putInt(key: String, value: Int?) =
putValue(key, value) { provider -> provider.putInt(key, value) }
putValue(key, value) { provider -> provider.putInt(key, value) }
fun putLong(key: String, value: Long?) =
putValue(key, value) { provider -> provider.putLong(key, value) }
putValue(key, value) { provider -> provider.putLong(key, value) }
fun putString(key: String, value: String?) =
putValue(key, value) { provider -> provider.putString(key, value) }
putValue(key, value) { provider -> provider.putString(key, value) }
fun remove(key: String) = putString(key, null)
/*** LIVE DATA ***/
/**
* Returns a [MutableLiveData] which is backed by the settings with the given key.
* An observer must be added to the returned [MutableLiveData] to make it active.
*/
fun getBooleanLive(key: String) = object : MutableLiveData<Boolean?>() {
private val preferenceChangeListener = OnChangeListener { updateValue() }
private fun updateValue() {
value = getBooleanOrNull(key)
}
// setValue is also called from postValue, so no need to override
override fun setValue(value: Boolean?) {
super.setValue(value)
putBoolean(key, value)
}
inner class SettingLiveData<T>(
val getValueOrNull: () -> T?
): LiveData<T>(), OnChangeListener {
override fun onActive() {
super.onActive()
updateValue()
addOnChangeListener(preferenceChangeListener)
addOnChangeListener(this)
update()
}
override fun onInactive() {
super.onInactive()
removeOnChangeListener(preferenceChangeListener)
removeOnChangeListener(this)
}
override fun onSettingsChanged() {
update()
}
@Synchronized
private fun update() {
val newValue = getValueOrNull()
if (value != newValue)
postValue(newValue)
}
}

View file

@ -4,287 +4,557 @@
package at.bitfire.davdroid.ui
import android.annotation.SuppressLint
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.drawable.InsetDrawable
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.text.InputType
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
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.CompositionLocalProvider
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.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
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.resource.TaskUtils
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import at.bitfire.davdroid.ui.intro.OpenSourcePage
import com.google.android.material.snackbar.Snackbar
import at.bitfire.davdroid.ui.widget.EditTextInputDialog
import at.bitfire.davdroid.ui.widget.MultipleChoiceInputDialog
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import at.bitfire.davdroid.ui.widget.Setting
import at.bitfire.davdroid.ui.widget.SettingsHeader
import at.bitfire.davdroid.ui.widget.SwitchSetting
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.ical4android.TaskProvider
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.net.URI
import java.net.URISyntaxException
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
class AppSettingsActivity: AppCompatActivity() {
companion object {
const val EXTRA_SCROLL_TO = "scrollTo"
/**
* Matches all language qualifiers with a region of three characters, which is not supported
* by Java's Locale.
* @see resourceQualifierToLanguageTag
*/
private val langRegex = Regex(".*-.{3}")
/**
* Converts the language qualifier given from Android to Java Locale language tag.
* @param lang The qualifier to convert. Example: `en`, `zh-rTW`...
* @return A correct language code to be introduced into [java.util.Locale.forLanguageTag].
*/
fun resourceQualifierToLanguageTag(lang: String): String {
// If the language qualifier is correct, return it
if (!lang.matches(langRegex)) return lang
// Otherwise, fix it
val hyphenIndex = lang.indexOf('-')
// Remove the first character of the 3 (rGB -> GB, rTW -> TW)
return lang.substring(0, hyphenIndex) + "-" + lang.substring(hyphenIndex + 2)
}
const val APP_SETTINGS_HELP_URL = "https://manual.davx5.com/settings.html#app-wide-settings"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val fragment = SettingsFragment()
fragment.arguments = intent.extras
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, fragment)
.commit()
setContent {
MdcTheme {
CompositionLocalProvider(LocalUriHandler provides SafeAndroidUriHandler(this)) {
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 = {
onNavigateUp()
}) {
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.getBatterySavingExempted().observeAsState(false).value,
onExemptFromBatterySaving = {
startActivity(Intent(
android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
))
}
)
AppSettings_Connection(
proxyType = model.settings.getIntLive(Settings.PROXY_TYPE).observeAsState(Settings.PROXY_TYPE_NONE).value,
onProxyTypeUpdated = { model.settings.putInt(Settings.PROXY_TYPE, it) },
proxyHostName = model.settings.getStringLive(Settings.PROXY_HOST).observeAsState(null).value,
onProxyHostNameUpdated = { model.settings.putString(Settings.PROXY_HOST, it) },
proxyPort = model.settings.getIntLive(Settings.PROXY_PORT).observeAsState(null).value,
onProxyPortUpdated = { model.settings.putInt(Settings.PROXY_PORT, it) }
)
AppSettings_Security(
distrustSystemCerts = model.settings.getBooleanLive(Settings.DISTRUST_SYSTEM_CERTIFICATES).observeAsState(false).value,
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.getIntLive(Settings.PREFERRED_THEME).observeAsState(Settings.PREFERRED_THEME_DEFAULT).value,
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.currentProviderLive(context).observeAsState().value
)
}
}
}
}
@Composable
fun AppSettings_Debugging(
verboseLogging: Boolean,
onUpdateVerboseLogging: (Boolean) -> Unit,
batterySavingExempted: Boolean,
onExemptFromBatterySaving: () -> 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,
enabled = !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)
) {
onExemptFromBatterySaving()
}
}
@Composable
@Preview
fun AppSettings_Debugging_Preview() {
Column {
AppSettings_Debugging(
verboseLogging = false,
onUpdateVerboseLogging = {},
batterySavingExempted = true,
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 }
)
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())
},
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
)
}
}
@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()
}
}
@AndroidEntryPoint
class SettingsFragment: PreferenceFragmentCompat(), SettingsManager.OnChangeListener {
@HiltViewModel
class Model @Inject constructor(
val context: Application,
val settings: SettingsManager
) : ViewModel() {
@Inject lateinit var settings: SettingsManager
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val onBatteryOptimizationResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
loadSettings()
}
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
addPreferencesFromResource(R.xml.settings_app)
// UI settings
findPreference<Preference>("notification_settings")!!.apply {
if (Build.VERSION.SDK_INT >= 26)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
startActivity(Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
})
false
}
else
isVisible = false
}
findPreference<Preference>("reset_hints")!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
resetHints()
false
}
// security settings
findPreference<Preference>("reset_certificates")!!.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
resetCertificates()
false
fun getBatterySavingExempted(): LiveData<Boolean> = object : LiveData<Boolean>() {
val receiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
update()
}
}
findPreference<EditTextPreference>(Settings.PROXY_HOST)!!.apply {
this.setOnBindEditTextListener {
it.inputType = InputType.TYPE_TEXT_VARIATION_URI
}
override fun onActive() {
context.registerReceiver(receiver, IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED))
update()
}
findPreference<EditTextPreference>(Settings.PROXY_PORT)!!.apply {
this.setOnBindEditTextListener {
it.inputType = InputType.TYPE_CLASS_NUMBER
}
override fun onInactive() {
context.unregisterReceiver(receiver)
}
arguments?.getString(EXTRA_SCROLL_TO)?.let { key ->
scrollToPreference(key)
}
}
override fun onStart() {
super.onStart()
settings.addOnChangeListener(this)
loadSettings()
}
override fun onStop() {
super.onStop()
settings.removeOnChangeListener(this)
}
@UiThread
private fun loadSettings() {
// debug settings
findPreference<SwitchPreferenceCompat>(Settings.BATTERY_OPTIMIZATION)!!.apply {
// battery optimization exists since Android 6 (API level 23)
val powerManager = requireActivity().getSystemService<PowerManager>()!!
val whitelisted = powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
isChecked = whitelisted
isEnabled = !whitelisted
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, nowChecked ->
if (nowChecked as Boolean)
onBatteryOptimizationResult.launch(Intent(
android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
))
false
}
}
// connection settings
val proxyType = settings.getInt(Settings.PROXY_TYPE)
findPreference<ListPreference>(Settings.PROXY_TYPE)!!.apply {
setValueIndex(entryValues.indexOf(proxyType.toString()))
summary = entry
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val proxyType = (newValue as String).toInt()
settings.putInt(Settings.PROXY_TYPE, proxyType)
false
}
}
findPreference<EditTextPreference>(Settings.PROXY_HOST)!!.apply {
isVisible = proxyType != Settings.PROXY_TYPE_SYSTEM && proxyType != Settings.PROXY_TYPE_NONE
isEnabled = settings.isWritable(Settings.PROXY_HOST)
val proxyHost = settings.getString(Settings.PROXY_HOST)
text = proxyHost
summary = proxyHost
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val host = newValue as String
try {
URI(null, host, null, null)
settings.putString(Settings.PROXY_HOST, host)
summary = host
false
} catch(e: URISyntaxException) {
Snackbar.make(requireView(), e.reason, Snackbar.LENGTH_LONG).show()
false
}
}
}
findPreference<EditTextPreference>(Settings.PROXY_PORT)!!.apply {
isVisible = proxyType != Settings.PROXY_TYPE_SYSTEM && proxyType != Settings.PROXY_TYPE_NONE
isEnabled = settings.isWritable(Settings.PROXY_PORT)
val proxyPort = settings.getInt(Settings.PROXY_PORT)
text = proxyPort.toString()
summary = proxyPort.toString()
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
try {
val port = (newValue as String).toInt()
if (port in 1..65535) {
settings.putInt(Settings.PROXY_PORT, port)
text = port.toString()
summary = port.toString()
false
} else
false
} catch(e: NumberFormatException) {
false
}
}
}
// security settings
findPreference<SwitchPreferenceCompat>(Settings.DISTRUST_SYSTEM_CERTIFICATES)!!
.isChecked = settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
// user interface settings
findPreference<ListPreference>(Settings.PREFERRED_THEME)!!.apply {
val mode = settings.getIntOrNull(Settings.PREFERRED_THEME) ?: Settings.PREFERRED_THEME_DEFAULT
setValueIndex(entryValues.indexOf(mode.toString()))
summary = entry
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val newMode = (newValue as String).toInt()
AppCompatDelegate.setDefaultNightMode(newMode)
settings.putInt(Settings.PREFERRED_THEME, newMode)
false
}
}
// integration settings
findPreference<Preference>(Settings.PREFERRED_TASKS_PROVIDER)!!.apply {
val pm = requireActivity().packageManager
val taskProvider = TaskUtils.currentProvider(requireActivity())
if (taskProvider != null) {
val tasksAppInfo = pm.getApplicationInfo(taskProvider.packageName, 0)
val inset = (24*resources.displayMetrics.density).roundToInt() // 24dp
icon = InsetDrawable(
tasksAppInfo.loadIcon(pm),
0, inset, inset, inset
)
summary = getString(R.string.app_settings_tasks_provider_synchronizing_with, tasksAppInfo.loadLabel(pm))
} else {
setIcon(R.drawable.ic_playlist_add_check)
setSummary(R.string.app_settings_tasks_provider_none)
}
setOnPreferenceClickListener {
startActivity(Intent(requireActivity(), TasksActivity::class.java))
false
private fun update() {
context.getSystemService<PowerManager>()?.let { powerManager ->
val exempted = powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
postValue(exempted)
}
}
}
override fun onSettingsChanged() {
// loadSettings must run in UI thread
CoroutineScope(Dispatchers.Main).launch {
if (isAdded)
loadSettings()
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()
}
private fun resetHints() {
fun resetCertificates() {
CustomCertStore.getInstance(context).clearUserDecisions()
}
fun resetHints() {
settings.remove(BatteryOptimizationsPage.Model.HINT_BATTERY_OPTIMIZATIONS)
settings.remove(BatteryOptimizationsPage.Model.HINT_AUTOSTART_PERMISSION)
settings.remove(TasksActivity.Model.HINT_OPENTASKS_NOT_INSTALLED)
settings.remove(OpenSourcePage.Model.SETTING_NEXT_DONATION_POPUP)
Snackbar.make(requireView(), R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show()
}
private fun resetCertificates() {
CustomCertStore.getInstance(requireActivity()).clearUserDecisions()
Snackbar.make(requireView(), getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show()
}
}
}
}

View file

@ -30,7 +30,6 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.UrlAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
@ -103,7 +102,7 @@ object UiUtils {
return false
}
fun setTheme(context: Context) {
fun updateTheme(context: Context) {
val settings = EntryPointAccessors.fromApplication(context, UiUtilsEntryPoint::class.java).settingsManager()
val mode = settings.getIntOrNull(Settings.PREFERRED_THEME) ?: Settings.PREFERRED_THEME_DEFAULT
AppCompatDelegate.setDefaultNightMode(mode)

View file

@ -55,6 +55,7 @@ import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage.Model.Companion.HINT_AUTOSTART_PERMISSION
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage.Model.Companion.HINT_BATTERY_OPTIMIZATIONS
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import at.bitfire.davdroid.util.PermissionUtils
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
@ -116,14 +117,14 @@ class BatteryOptimizationsPage: IntroPage {
BatteryOptimizationsContent(
dontShowBattery = hintBatteryOptimizations == false,
onChangeDontShowBattery = {
model.hintBatteryOptimizations.value = !it
model.settings.putBoolean(HINT_BATTERY_OPTIMIZATIONS, !it)
},
isExempted = isExempted,
shouldBeExempted = shouldBeExempted,
onChangeShouldBeExempted = model.shouldBeExempted::postValue,
dontShowAutostart = hintAutostartPermission == false,
onChangeDontShowAutostart = {
model.hintAutostartPermission.value = !it
model.settings.putBoolean(HINT_AUTOSTART_PERMISSION, !it)
},
manufacturerWarning = Model.manufacturerWarning
)
@ -182,18 +183,16 @@ class BatteryOptimizationsPage: IntroPage {
val shouldBeExempted = MutableLiveData<Boolean>()
val isExempted = MutableLiveData<Boolean>()
val hintBatteryOptimizations = settings.getBooleanLive(HINT_BATTERY_OPTIMIZATIONS)
val hintAutostartPermission = settings.getBooleanLive(HINT_AUTOSTART_PERMISSION)
private val batteryOptimizationsReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
checkBatteryOptimizations()
}
}
val hintAutostartPermission = settings.getBooleanLive(HINT_AUTOSTART_PERMISSION)
init {
// There's an undocumented intent that is sent when the battery optimization whitelist changes.
val intentFilter = IntentFilter("android.os.action.POWER_SAVE_WHITELIST_CHANGED")
val intentFilter = IntentFilter(PermissionUtils.ACTION_POWER_SAVE_WHITELIST_CHANGED)
context.registerReceiver(batteryOptimizationsReceiver, intentFilter)
checkBatteryOptimizations()

View file

@ -0,0 +1,165 @@
package at.bitfire.davdroid.ui.widget
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun EditTextInputDialog(
title: String,
initialValue: String? = null,
keyboardType: KeyboardType = KeyboardType.Text,
onValueEntered: (String) -> Unit = {},
onDismiss: () -> Unit = {},
) {
var textValue by remember {
mutableStateOf(TextFieldValue(
initialValue ?: "", selection = TextRange(initialValue?.length ?: 0)
))
}
val focusRequester = remember { FocusRequester() }
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
title,
style = MaterialTheme.typography.h6
)
},
text = {
TextField(
value = textValue,
onValueChange = { textValue = it },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
onValueEntered(textValue.text)
onDismiss()
}
),
modifier = Modifier.focusRequester(focusRequester)
)
},
confirmButton = {
TextButton(
onClick = {
onValueEntered(textValue.text)
onDismiss()
},
enabled = textValue.text != initialValue
) {
Text(stringResource(android.R.string.ok).uppercase())
}
},
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text(stringResource(android.R.string.cancel).uppercase())
}
}
)
var requestFocus = remember { true }
LaunchedEffect(requestFocus) {
if (requestFocus) {
focusRequester.requestFocus()
requestFocus = false
}
}
}
@Composable
@Preview
fun EditTextInputDialog_Preview() {
EditTextInputDialog(
title = "Enter Some Text",
initialValue = "initial value"
)
}
@Composable
fun MultipleChoiceInputDialog(
title: String,
namesAndValues: List<Pair<String, String>>,
initialValue: String? = null,
onValueSelected: (String) -> Unit = {},
onDismiss: () -> Unit = {},
) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
title,
style = MaterialTheme.typography.h6
)
},
text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
for ((name, value) in namesAndValues)
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = value == initialValue,
onClick = {
onValueSelected(value)
onDismiss()
}
)
Text(
name,
style = MaterialTheme.typography.body1,
modifier = Modifier.clickable {
onValueSelected(value)
onDismiss()
}
)
}
}
},
buttons = {}
)
}
@Composable
@Preview
fun MultipleChoiceInputDialog_Preview() {
MultipleChoiceInputDialog(
title = "Some Title",
namesAndValues = listOf(
"Some Name" to "Some Value",
"Some Other Name" to "Some Other Value"
)
)
}

View file

@ -0,0 +1,168 @@
package at.bitfire.davdroid.ui.widget
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.Switch
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Folder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun SettingsHeader(divider: Boolean = false, content: @Composable () -> Unit) {
if (divider)
Divider(Modifier.padding(vertical = 8.dp))
Row(
Modifier
.padding(top = 16.dp, start = 52.dp, end = 16.dp, bottom = 8.dp)
.fillMaxWidth()
) {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.body1.copy(
color = MaterialTheme.colors.secondary
)
) {
content()
}
}
}
@Composable
@Preview
fun SettingsHeader_Sample() {
Column {
SettingsHeader(divider = true) {
Text("Some Settings Section")
}
}
}
@Composable
fun Setting(
icon: @Composable () -> Unit,
name: @Composable () -> Unit,
summary: String?,
end: @Composable () -> Unit = {},
enabled: Boolean = true,
onClick: () -> Unit = {}
) {
var modifier = Modifier.fillMaxWidth()
if (enabled)
modifier = modifier.clickable(onClick = onClick)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.padding(vertical = 8.dp)
) {
Box(
modifier = Modifier
.width(44.dp)
.padding(4.dp),
contentAlignment = Alignment.Center
) {
icon()
}
Column(
Modifier
.padding(start = 8.dp)
.weight(1f)
) {
name()
if (summary != null)
Text(summary, style = MaterialTheme.typography.body2)
}
end()
}
}
@Composable
fun Setting(
name: String,
summary: String? = null,
icon: ImageVector? = null,
onClick: () -> Unit = {}
) {
Setting(
icon = {
if (icon != null)
Icon(icon, contentDescription = name)
},
name = {
Text(name, style = MaterialTheme.typography.body1)
},
summary = summary,
onClick = onClick
)
}
@Composable
@Preview
fun Setting_Sample() {
Setting(
icon = Icons.Default.Folder,
name = "Setting",
summary = "Currently off"
)
}
@Composable
fun SwitchSetting(
checked: Boolean,
name: String,
summaryOn: String? = null,
summaryOff: String? = null,
enabled: Boolean = true,
icon: ImageVector? = null,
onCheckedChange: (Boolean) -> Unit = {}
) {
Setting(
icon = {
if (icon != null)
Icon(icon, name)
},
name = {
Text(name)
},
summary = if (checked) summaryOn else summaryOff,
end = {
Switch(
checked = checked,
enabled = enabled,
onCheckedChange = onCheckedChange,
modifier = Modifier.padding(horizontal = 4.dp)
)
},
enabled = enabled
) {
onCheckedChange(!checked)
}
}
@Composable
@Preview
fun SwitchSetting_Sample() {
SwitchSetting(
name = "Some Switched Setting",
checked = true
)
}

View file

@ -26,6 +26,9 @@ import at.bitfire.davdroid.ui.PermissionsActivity
object PermissionUtils {
/** There's an undocumented intent that is sent when the battery optimization whitelist changes. */
const val ACTION_POWER_SAVE_WHITELIST_CHANGED = "android.os.action.POWER_SAVE_WHITELIST_CHANGED"
val CONTACT_PERMISSIONS = arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS

View file

@ -147,8 +147,8 @@
<string name="app_settings_logging_on">Дневникът е включен</string>
<string name="app_settings_logging_off">Дневникът е изключен</string>
<string name="app_settings_battery_optimization">Оптимизиране на батерията</string>
<string name="app_settings_battery_optimization_whitelisted">Приложението е в белия списък (препоръчително)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Приложението не е в белия списък (непрепоръчително)</string>
<string name="app_settings_battery_optimization_exempted">Приложението е в белия списък (препоръчително)</string>
<string name="app_settings_battery_optimization_optimized">Приложението не е в белия списък (непрепоръчително)</string>
<string name="app_settings_connection">Връзка</string>
<string name="app_settings_proxy">Вид на сървъра на прокси</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">Registre actiu</string>
<string name="app_settings_logging_off">Registre inactiu</string>
<string name="app_settings_battery_optimization">Optimització de la bateria</string>
<string name="app_settings_battery_optimization_whitelisted">L\'aplicació és a la llista blanca (recomanat)</string>
<string name="app_settings_battery_optimization_not_whitelisted">L\'aplicació no és a la llista blanca (no recomanat)</string>
<string name="app_settings_battery_optimization_exempted">L\'aplicació és a la llista blanca (recomanat)</string>
<string name="app_settings_battery_optimization_optimized">L\'aplicació no és a la llista blanca (no recomanat)</string>
<string name="app_settings_connection">Connexió</string>
<string name="app_settings_proxy">Tipus de servidor intermediari</string>
<string-array name="app_settings_proxy_types">

View file

@ -152,8 +152,8 @@
<string name="app_settings_logging_on">Zaznamenávání událostí je aktivní</string>
<string name="app_settings_logging_off">Zaznamenávání událostí je vypnuté</string>
<string name="app_settings_battery_optimization">Optimalizace akumulátoru</string>
<string name="app_settings_battery_optimization_whitelisted">Aplikace je zařazena na seznam výjimek (doporučeno)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Aplikace není zařazena na seznam výjimek (není doporučeno)</string>
<string name="app_settings_battery_optimization_exempted">Aplikace je zařazena na seznam výjimek (doporučeno)</string>
<string name="app_settings_battery_optimization_optimized">Aplikace není zařazena na seznam výjimek (není doporučeno)</string>
<string name="app_settings_connection">Připojení</string>
<string name="app_settings_proxy">Typ proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -151,8 +151,8 @@ Lav lagerplads. Android vil ikke synkronisere lokale ændringer med det samme, m
<string name="app_settings_logging_on">Logning er aktiv</string>
<string name="app_settings_logging_off">Logning er deaktiveret</string>
<string name="app_settings_battery_optimization">Batteri optimering</string>
<string name="app_settings_battery_optimization_whitelisted">App er hvid listet (anbefalet)</string>
<string name="app_settings_battery_optimization_not_whitelisted">App er ikke hvid listet (ikke anbefalet)</string>
<string name="app_settings_battery_optimization_exempted">App er hvid listet (anbefalet)</string>
<string name="app_settings_battery_optimization_optimized">App er ikke hvid listet (ikke anbefalet)</string>
<string name="app_settings_connection">Forbindelse</string>
<string name="app_settings_proxy">Proxy type</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">Protokollierung läuft</string>
<string name="app_settings_logging_off">Keine Protokollierung</string>
<string name="app_settings_battery_optimization">Akku-Optimierung</string>
<string name="app_settings_battery_optimization_whitelisted">App ist ausgenommen (empfohlen)</string>
<string name="app_settings_battery_optimization_not_whitelisted">App ist nicht ausgenommen (nicht empfohlen)</string>
<string name="app_settings_battery_optimization_exempted">App ist ausgenommen (empfohlen)</string>
<string name="app_settings_battery_optimization_optimized">App ist nicht ausgenommen (nicht empfohlen)</string>
<string name="app_settings_connection">Verbindung</string>
<string name="app_settings_proxy">Proxy-Typ</string>
<string-array name="app_settings_proxy_types">

View file

@ -149,8 +149,8 @@
<string name="app_settings_logging_on">Logging is active</string>
<string name="app_settings_logging_off">Logging is disabled</string>
<string name="app_settings_battery_optimization">Battery optimisation</string>
<string name="app_settings_battery_optimization_whitelisted">App is whitelisted (recommended)</string>
<string name="app_settings_battery_optimization_not_whitelisted">App is not whitelisted (not recommended)</string>
<string name="app_settings_battery_optimization_exempted">App is whitelisted (recommended)</string>
<string name="app_settings_battery_optimization_optimized">App is not whitelisted (not recommended)</string>
<string name="app_settings_connection">Connection</string>
<string name="app_settings_proxy">Proxy type</string>
<string-array name="app_settings_proxy_types">

View file

@ -137,8 +137,8 @@
<string name="app_settings_logging_on">El registro está activo</string>
<string name="app_settings_logging_off">El registro está deshabilitado</string>
<string name="app_settings_battery_optimization">Optimización de batería</string>
<string name="app_settings_battery_optimization_whitelisted">La app está la lista blanca (recomendado)</string>
<string name="app_settings_battery_optimization_not_whitelisted">La app no está en la lista blanca (no recomendado)</string>
<string name="app_settings_battery_optimization_exempted">La app está la lista blanca (recomendado)</string>
<string name="app_settings_battery_optimization_optimized">La app no está en la lista blanca (no recomendado)</string>
<string name="app_settings_connection">Conexión</string>
<string name="app_settings_proxy">Tipo de proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">Erregistratzea gaituta dago</string>
<string name="app_settings_logging_off">Erregistratzea desgaituta dago</string>
<string name="app_settings_battery_optimization">Bateria optimizazioa</string>
<string name="app_settings_battery_optimization_whitelisted">Aplikazioa zerrenda zurian dago (gomendatuta)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Aplikazioa ez dago zerrenda zurian (ez gomendatuta)</string>
<string name="app_settings_battery_optimization_exempted">Aplikazioa zerrenda zurian dago (gomendatuta)</string>
<string name="app_settings_battery_optimization_optimized">Aplikazioa ez dago zerrenda zurian (ez gomendatuta)</string>
<string name="app_settings_connection">Konexioa</string>
<string name="app_settings_proxy">Proxy mota</string>
<string-array name="app_settings_proxy_types">

View file

@ -146,8 +146,8 @@
<string name="app_settings_logging_on">ورود به سیستم فعال است</string>
<string name="app_settings_logging_off">ورود به سیستم غیرفعال است</string>
<string name="app_settings_battery_optimization">بهینه ساز باتری</string>
<string name="app_settings_battery_optimization_whitelisted">برنامه در لیست سفید قرار دارد (توصیه می شود).</string>
<string name="app_settings_battery_optimization_not_whitelisted">برنامه در لیست سفید قرار ندارد (توصیه نمی شود).</string>
<string name="app_settings_battery_optimization_exempted">برنامه در لیست سفید قرار دارد (توصیه می شود).</string>
<string name="app_settings_battery_optimization_optimized">برنامه در لیست سفید قرار ندارد (توصیه نمی شود).</string>
<string name="app_settings_connection">ارتباط</string>
<string name="app_settings_proxy">نوع پروکسی</string>
<string-array name="app_settings_proxy_types">

View file

@ -152,8 +152,8 @@
<string name="app_settings_logging_on">La journalisation est activée</string>
<string name="app_settings_logging_off">La journalisation est désactivée</string>
<string name="app_settings_battery_optimization">Optimisation de la batterie</string>
<string name="app_settings_battery_optimization_whitelisted">L\'App est dans la liste blanche (recommandé)</string>
<string name="app_settings_battery_optimization_not_whitelisted">L\'App n\'est pas dans la liste blanche (non recommandé)</string>
<string name="app_settings_battery_optimization_exempted">L\'App est dans la liste blanche (recommandé)</string>
<string name="app_settings_battery_optimization_optimized">L\'App n\'est pas dans la liste blanche (non recommandé)</string>
<string name="app_settings_connection">Connexion</string>
<string name="app_settings_proxy">Type de Proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">O rexistro está activo</string>
<string name="app_settings_logging_off">O rexistro está desactivado</string>
<string name="app_settings_battery_optimization">Optimización da batería</string>
<string name="app_settings_battery_optimization_whitelisted">App está permitida (recomendado)</string>
<string name="app_settings_battery_optimization_not_whitelisted">App sen permiso (non recomendado)</string>
<string name="app_settings_battery_optimization_exempted">App está permitida (recomendado)</string>
<string name="app_settings_battery_optimization_optimized">App sen permiso (non recomendado)</string>
<string name="app_settings_connection">Conexión</string>
<string name="app_settings_proxy">Tipo de Proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -152,8 +152,8 @@
<string name="app_settings_logging_on">Naplózás bekapcsolva</string>
<string name="app_settings_logging_off">Naplózás kikapcsolva</string>
<string name="app_settings_battery_optimization">Akkumulátorhasználat optimalizálása</string>
<string name="app_settings_battery_optimization_whitelisted">Az optimalizálás kikapcsolva (ajánlott)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Az optimalizálás bekapcsolva (nem ajánlott)</string>
<string name="app_settings_battery_optimization_exempted">Az optimalizálás kikapcsolva (ajánlott)</string>
<string name="app_settings_battery_optimization_optimized">Az optimalizálás bekapcsolva (nem ajánlott)</string>
<string name="app_settings_connection">Kapcsolat</string>
<string name="app_settings_proxy">A proxy típusa</string>
<string-array name="app_settings_proxy_types">

View file

@ -134,8 +134,8 @@
<string name="app_settings_logging_on">Log attivo</string>
<string name="app_settings_logging_off">Log disabilitato</string>
<string name="app_settings_battery_optimization">Ottimizzazione batteria</string>
<string name="app_settings_battery_optimization_whitelisted">La app è in lista bianca (raccomandato)</string>
<string name="app_settings_battery_optimization_not_whitelisted">La app non è in lista bianca (non raccomandato)</string>
<string name="app_settings_battery_optimization_exempted">La app è in lista bianca (raccomandato)</string>
<string name="app_settings_battery_optimization_optimized">La app non è in lista bianca (non raccomandato)</string>
<string name="app_settings_connection">Connessione</string>
<string name="app_settings_proxy">Tipo di proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">ログ取得が有効</string>
<string name="app_settings_logging_off">ログ取得が無効</string>
<string name="app_settings_battery_optimization">バッテリー最適化</string>
<string name="app_settings_battery_optimization_whitelisted">このアプリでは無効 (推奨)</string>
<string name="app_settings_battery_optimization_not_whitelisted">有効 (非推奨)</string>
<string name="app_settings_battery_optimization_exempted">このアプリでは無効 (推奨)</string>
<string name="app_settings_battery_optimization_optimized">有効 (非推奨)</string>
<string name="app_settings_connection">接続</string>
<string name="app_settings_proxy">プロキシーの種類</string>
<string-array name="app_settings_proxy_types">

View file

@ -136,8 +136,8 @@
<string name="app_settings_logging_on">logging이 활성되었습니다.</string>
<string name="app_settings_logging_off">logging이 비활성화되었습니다.</string>
<string name="app_settings_battery_optimization">배터리 최적화</string>
<string name="app_settings_battery_optimization_whitelisted">앱이 허용목록에 포함(권장)</string>
<string name="app_settings_battery_optimization_not_whitelisted">앱이 허용목록에 미포함(권장하지 않음)</string>
<string name="app_settings_battery_optimization_exempted">앱이 허용목록에 포함(권장)</string>
<string name="app_settings_battery_optimization_optimized">앱이 허용목록에 미포함(권장하지 않음)</string>
<string name="app_settings_connection">연결</string>
<string name="app_settings_proxy">프록시 타입</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">Loggen is actief</string>
<string name="app_settings_logging_off">Loggen is niet actief</string>
<string name="app_settings_battery_optimization">Batterijoptimalisatie</string>
<string name="app_settings_battery_optimization_whitelisted">Onbeperkt batterijgebruik toestaan (aanbevolen)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Beperkt batterijgebruik toestaan (niet aanbevolen)</string>
<string name="app_settings_battery_optimization_exempted">Onbeperkt batterijgebruik toestaan (aanbevolen)</string>
<string name="app_settings_battery_optimization_optimized">Beperkt batterijgebruik toestaan (niet aanbevolen)</string>
<string name="app_settings_connection">Verbinding</string>
<string name="app_settings_proxy">Proxy type</string>
<string-array name="app_settings_proxy_types">

View file

@ -152,8 +152,8 @@
<string name="app_settings_logging_on">Logowanie jest włączone</string>
<string name="app_settings_logging_off">Logowanie jest wyłączone</string>
<string name="app_settings_battery_optimization">Optymalizacja baterii</string>
<string name="app_settings_battery_optimization_whitelisted">Aplikacja jest na białej liście (zalecane)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Aplikacja nie znajduje się na białej liście (niezalecane)</string>
<string name="app_settings_battery_optimization_exempted">Aplikacja jest na białej liście (zalecane)</string>
<string name="app_settings_battery_optimization_optimized">Aplikacja nie znajduje się na białej liście (niezalecane)</string>
<string name="app_settings_connection">Łączność</string>
<string name="app_settings_proxy">Typ proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">Înregistrarea este activă</string>
<string name="app_settings_logging_off">Înregistrarea este dezactivată</string>
<string name="app_settings_battery_optimization">Optimizarea bateriei</string>
<string name="app_settings_battery_optimization_whitelisted">Aplicația este inclusă în lista albă (recomandat)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Aplicația nu este inclusă în lista albă (nu este recomandat)</string>
<string name="app_settings_battery_optimization_exempted">Aplicația este inclusă în lista albă (recomandat)</string>
<string name="app_settings_battery_optimization_optimized">Aplicația nu este inclusă în lista albă (nu este recomandat)</string>
<string name="app_settings_connection">Conexiune</string>
<string name="app_settings_proxy">Tip proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">Логирование активно</string>
<string name="app_settings_logging_off">Логирование отключено</string>
<string name="app_settings_battery_optimization">Оптимизация батареи</string>
<string name="app_settings_battery_optimization_whitelisted">Приложение добавлено в белый список (рекомендуется)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Приложение не добавлено в белый список (не рекомендуется)</string>
<string name="app_settings_battery_optimization_exempted">Приложение добавлено в белый список (рекомендуется)</string>
<string name="app_settings_battery_optimization_optimized">Приложение не добавлено в белый список (не рекомендуется)</string>
<string name="app_settings_connection">Соединение</string>
<string name="app_settings_proxy">Тип прокси</string>
<string-array name="app_settings_proxy_types">

View file

@ -152,8 +152,8 @@
<string name="app_settings_logging_on">Loggning är påslagen</string>
<string name="app_settings_logging_off">Loggning är avstängd</string>
<string name="app_settings_battery_optimization">Batterioptimering</string>
<string name="app_settings_battery_optimization_whitelisted">Appen är vitlistad (rekommenderat)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Appen är inte vitlistad (rekommenderas ej)</string>
<string name="app_settings_battery_optimization_exempted">Appen är vitlistad (rekommenderat)</string>
<string name="app_settings_battery_optimization_optimized">Appen är inte vitlistad (rekommenderas ej)</string>
<string name="app_settings_connection">Anslutning</string>
<string name="app_settings_proxy">Proxy typ</string>
<string-array name="app_settings_proxy_types">

View file

@ -137,8 +137,8 @@
<string name="app_settings_logging_on">Ghi nhật ký đang hoạt động</string>
<string name="app_settings_logging_off">Ghi nhật ký đã tắt</string>
<string name="app_settings_battery_optimization">Tối ưu hoá pin</string>
<string name="app_settings_battery_optimization_whitelisted">Ứng dụng không được tối ưu (khuyên dùng)</string>
<string name="app_settings_battery_optimization_not_whitelisted">Ứng dụng được tối ưu (không khuyên dùng)</string>
<string name="app_settings_battery_optimization_exempted">Ứng dụng không được tối ưu (khuyên dùng)</string>
<string name="app_settings_battery_optimization_optimized">Ứng dụng được tối ưu (không khuyên dùng)</string>
<string name="app_settings_connection">Kết nối</string>
<string name="app_settings_proxy">Loại proxy</string>
<string-array name="app_settings_proxy_types">

View file

@ -154,8 +154,8 @@
<string name="app_settings_logging_on">日志记录已开启</string>
<string name="app_settings_logging_off">日志记录已禁用</string>
<string name="app_settings_battery_optimization">电池优化</string>
<string name="app_settings_battery_optimization_whitelisted">将应用加入白名单(推荐)</string>
<string name="app_settings_battery_optimization_not_whitelisted">不把应用加入白名单(不推荐)</string>
<string name="app_settings_battery_optimization_exempted">将应用加入白名单(推荐)</string>
<string name="app_settings_battery_optimization_optimized">不把应用加入白名单(不推荐)</string>
<string name="app_settings_connection">连接</string>
<string name="app_settings_proxy">代理类型</string>
<string-array name="app_settings_proxy_types">

View file

@ -170,8 +170,8 @@
<string name="app_settings_logging_on">Logging is active</string>
<string name="app_settings_logging_off">Logging is disabled</string>
<string name="app_settings_battery_optimization">Battery optimization</string>
<string name="app_settings_battery_optimization_whitelisted">App is whitelisted (recommended)</string>
<string name="app_settings_battery_optimization_not_whitelisted">App is not whitelisted (not recommended)</string>
<string name="app_settings_battery_optimization_exempted">App is exempted (recommended)</string>
<string name="app_settings_battery_optimization_optimized">Battery restrictions apply (not recommended)</string>
<string name="app_settings_connection">Connection</string>
<string name="app_settings_proxy">Proxy type</string>
<string-array name="app_settings_proxy_types">

View file

@ -1,116 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/app_settings_debug">
<Preference
android:title="@string/app_settings_show_debug_info"
android:summary="@string/app_settings_show_debug_info_details"
android:icon="@drawable/ic_bug_report">
<intent
android:targetPackage="at.bitfire.davdroid"
android:targetClass="at.bitfire.davdroid.ui.DebugInfoActivity"/>
</Preference>
<SwitchPreferenceCompat
android:key="log_to_file"
android:title="@string/app_settings_logging"
android:icon="@drawable/ic_adb"
android:summaryOn="@string/app_settings_logging_on"
android:summaryOff="@string/app_settings_logging_off"/>
<SwitchPreferenceCompat
android:key="battery_optimization"
android:title="@string/app_settings_battery_optimization"
android:summaryOn="@string/app_settings_battery_optimization_whitelisted"
android:summaryOff="@string/app_settings_battery_optimization_not_whitelisted"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/app_settings_connection">
<ListPreference
android:key="proxy_type"
android:title="@string/app_settings_proxy"
android:entries="@array/app_settings_proxy_types"
android:entryValues="@array/app_settings_proxy_type_values"
android:persistent="false" />
<EditTextPreference
android:key="proxy_host"
android:title="@string/app_settings_proxy_host"
android:inputType="textUri"
android:persistent="false" />
<EditTextPreference
android:key="proxy_port"
android:title="@string/app_settings_proxy_port"
android:inputType="number"
android:persistent="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/app_settings_security">
<SwitchPreferenceCompat
android:key="distrust_system_certs"
android:title="@string/app_settings_distrust_system_certs"
android:summaryOn="@string/app_settings_distrust_system_certs_on"
android:summaryOff="@string/app_settings_distrust_system_certs_off" />
<Preference
android:key="reset_certificates"
android:title="@string/app_settings_reset_certificates"
android:summary="@string/app_settings_reset_certificates_summary"/>
<Preference
android:title="@string/app_settings_security_app_permissions"
android:summary="@string/app_settings_security_app_permissions_summary">
<intent
android:targetPackage="at.bitfire.davdroid"
android:targetClass="at.bitfire.davdroid.ui.PermissionsActivity"/>
</Preference>
</PreferenceCategory>
<PreferenceCategory android:title="@string/app_settings_user_interface">
<Preference
android:key="notification_settings"
android:icon="@drawable/ic_notifications"
android:title="@string/app_settings_notification_settings"
android:summary="@string/app_settings_notification_settings_summary"/>
<ListPreference
android:key="preferred_theme"
android:icon="@drawable/ic_invert_colors"
android:title="@string/app_settings_theme_title"
android:entries="@array/app_settings_theme_names"
android:entryValues="@array/app_settings_theme_values"
android:persistent="false" />
<!-- <ListPreference
android:key="language"
android:icon="@drawable/ic_language"
android:title="@string/app_settings_language_title"
android:persistent="false" /> -->
<Preference
android:key="reset_hints"
android:title="@string/app_settings_reset_hints"
android:summary="@string/app_settings_reset_hints_summary"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/app_settings_integration">
<Preference
android:key="preferred_tasks_provider"
android:icon="@drawable/ic_playlist_add_check"
android:title="@string/app_settings_tasks_provider"
android:summary="@string/app_settings_tasks_provider_synchronizing_with" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>