mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-22 03:01:24 +00:00
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:
parent
66f0075cc9
commit
c8a0128842
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
168
app/src/main/kotlin/at/bitfire/davdroid/ui/widget/Settings.kt
Normal file
168
app/src/main/kotlin/at/bitfire/davdroid/ui/widget/Settings.kt
Normal 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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue