Commit 7d468996 authored by Ricki Hirner's avatar Ricki Hirner

Refactor Settings provider

* don't use a separate :sync process anymore, so that settings management doesn't need IPC
* remove Settings service and IPC, use singleton with application Context instead
* adapt default number of sync worker threads
* library updates
parent b863d355
Pipeline #41342525 passed with stages
in 12 minutes and 31 seconds
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.settings
import android.content.Context
import at.bitfire.davdroid.log.Logger
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
class Settings(
appContext: Context
) {
companion object {
// settings keys and default values
const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
const val DISTRUST_SYSTEM_CERTIFICATES_DEFAULT = false
const val OVERRIDE_PROXY = "override_proxy"
const val OVERRIDE_PROXY_DEFAULT = false
const val OVERRIDE_PROXY_HOST = "override_proxy_host"
const val OVERRIDE_PROXY_PORT = "override_proxy_port"
const val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
const val OVERRIDE_PROXY_PORT_DEFAULT = 8118
private var singleton: Settings? = null
fun getInstance(context: Context): Settings {
singleton?.let { return it }
val newInstance = Settings(context.applicationContext)
singleton = newInstance
return newInstance
}
}
private val providers = LinkedList<SettingsProvider>()
private val observers = LinkedList<WeakReference<OnChangeListener>>()
init {
ServiceLoader.load(ISettingsProviderFactory::class.java).forEach { factory ->
providers.addAll(factory.getProviders(appContext))
}
}
fun forceReload() {
providers.forEach {
it.forceReload()
}
onSettingsChanged()
}
/*** OBSERVERS ***/
fun addOnChangeListener(observer: OnChangeListener) {
observers += WeakReference(observer)
}
fun removeOnChangeListener(observer: OnChangeListener) {
observers.removeAll { it.get() == null || it.get() == observer }
}
fun onSettingsChanged() {
observers.mapNotNull { it.get() }.forEach {
it.onSettingsChanged()
}
}
/*** SETTINGS ACCESS ***/
fun has(key: String): Boolean {
Logger.log.fine("Looking for setting $key")
var result = false
for (provider in providers)
try {
val (value, further) = provider.has(key)
Logger.log.finer("${provider::class.java.simpleName}: has $key = $value, continue: $further")
if (value) {
result = true
break
}
if (!further)
break
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't look up setting in $provider", e)
}
Logger.log.fine("Looking for setting $key -> $result")
return result
}
private fun<T> getValue(key: String, reader: (SettingsProvider) -> Pair<T?, Boolean>): T? {
Logger.log.fine("Looking up setting $key")
var result: T? = null
for (provider in providers)
try {
val (value, further) = reader(provider)
Logger.log.finer("${provider::class.java.simpleName}: value = $value, continue: $further")
value?.let { result = it }
if (!further)
break
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't read setting from $provider", e)
}
Logger.log.fine("Looked up setting $key -> $result")
return result
}
fun getBoolean(key: String) =
getValue(key) { provider -> provider.getBoolean(key) }
fun getInt(key: String) =
getValue(key) { provider -> provider.getInt(key) }
fun getLong(key: String) =
getValue(key) { provider -> provider.getLong(key) }
fun getString(key: String) =
getValue(key) { provider -> provider.getString(key) }
fun isWritable(key: String): Boolean {
for (provider in providers) {
val (value, further) = provider.isWritable(key)
if (value)
return true
if (!further)
return false
}
return false
}
private fun<T> putValue(key: String, value: T?, writer: (SettingsProvider) -> Boolean): Boolean {
Logger.log.fine("Trying to write setting $key = $value")
for (provider in providers) {
val (writable, further) = provider.isWritable(key)
Logger.log.finer("${provider::class.java.simpleName}: writable = $writable, continue: $further")
if (writable)
return try {
writer(provider)
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't write setting to $provider", e)
false
}
if (!further)
return false
}
return false
}
fun putBoolean(key: String, value: Boolean?) =
putValue(key, value) { provider -> provider.putBoolean(key, value) }
fun putInt(key: String, value: Int?) =
putValue(key, value) { provider -> provider.putInt(key, value) }
fun putLong(key: String, value: Long?) =
putValue(key, value) { provider -> provider.putLong(key, value) }
fun putString(key: String, value: String?) =
putValue(key, value) { provider -> provider.putString(key, value) }
fun remove(key: String): Boolean {
var deleted = false
providers.forEach { deleted = deleted || it.remove(key) }
return deleted
}
interface OnChangeListener {
fun onSettingsChanged()
}
}
......@@ -33,7 +33,6 @@ android {
productFlavors {
standard {
versionName "2.0.7-ose"
buildConfigField "boolean", "customCerts", "true"
}
}
......@@ -83,7 +82,7 @@ dependencies {
implementation 'com.github.yukuku:ambilwarna:2.0.1'
implementation 'com.mikepenz:aboutlibraries:6.2.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.1'
implementation 'commons-io:commons-io:2.6'
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'org.apache.commons:commons-lang3:3.8.1'
......@@ -93,8 +92,8 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.0'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
testImplementation 'junit:junit:4.12'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
}
......@@ -8,32 +8,31 @@
package at.bitfire.davdroid.settings
import at.bitfire.davdroid.App
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
class DefaultsProviderTest {
class DefaultsSettingsProviderTest {
private val provider: Provider = DefaultsProvider()
private val provider: SettingsProvider = DefaultsProvider()
@Test
fun testHas() {
assertEquals(Pair(false, true), provider.has("notExisting"))
assertEquals(Pair(true, true), provider.has(App.OVERRIDE_PROXY))
assertEquals(Pair(true, true), provider.has(Settings.OVERRIDE_PROXY))
}
@Test
fun testGet() {
assertEquals(Pair("localhost", true), provider.getString(App.OVERRIDE_PROXY_HOST))
assertEquals(Pair(8118, true), provider.getInt(App.OVERRIDE_PROXY_PORT))
assertEquals(Pair("localhost", true), provider.getString(Settings.OVERRIDE_PROXY_HOST))
assertEquals(Pair(8118, true), provider.getInt(Settings.OVERRIDE_PROXY_PORT))
}
@Test
fun testPutRemove() {
assertEquals(Pair(false, true), provider.isWritable(App.OVERRIDE_PROXY))
assertFalse(provider.putBoolean(App.OVERRIDE_PROXY, true))
assertFalse(provider.remove(App.OVERRIDE_PROXY))
assertEquals(Pair(false, true), provider.isWritable(Settings.OVERRIDE_PROXY))
assertFalse(provider.putBoolean(Settings.OVERRIDE_PROXY, true))
assertFalse(provider.remove(Settings.OVERRIDE_PROXY))
}
}
\ No newline at end of file
......@@ -9,8 +9,6 @@
package at.bitfire.davdroid.settings
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.App
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
......@@ -18,25 +16,19 @@ import org.junit.Test
class SettingsTest {
lateinit var settings: Settings.Stub
lateinit var settings: Settings
@Before
fun init() {
settings = Settings.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)!!
fun initialize() {
settings = Settings.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)
}
@After
fun shutdown() {
settings.close()
}
@Test
fun testHas() {
assertFalse(settings.has("notExisting"))
// provided by DefaultsProvider
assertTrue(settings.has(App.OVERRIDE_PROXY))
assertTrue(settings.has(Settings.OVERRIDE_PROXY))
}
}
\ No newline at end of file
......@@ -57,7 +57,6 @@
tools:ignore="UnusedAttribute">
<service android:name=".DavService"/>
<service android:name=".settings.Settings"/>
<activity
android:name=".ui.AccountsActivity"
......@@ -130,7 +129,6 @@
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
......@@ -143,7 +141,6 @@
<service
android:name=".syncadapter.TasksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
......@@ -175,7 +172,6 @@
<service
android:name=".syncadapter.AddressBooksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
......@@ -188,7 +184,6 @@
<service
android:name=".syncadapter.ContactsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
......
package at.bitfire.davdroid.settings;
import at.bitfire.davdroid.settings.ISettingsObserver;
interface ISettings {
void forceReload();
boolean has(String key);
boolean getBoolean(String key, boolean defaultValue);
int getInt(String key, int defaultValue);
long getLong(String key, long defaultValue);
String getString(String key, String defaultValue);
boolean isWritable(String key);
boolean putBoolean(String key, boolean value);
boolean putInt(String key, int value);
boolean putLong(String key, long value);
boolean putString(String key, String value);
boolean remove(String key);
void registerObserver(ISettingsObserver observer);
void unregisterObserver(ISettingsObserver observer);
}
package at.bitfire.davdroid.settings;
interface ISettingsObserver {
void onSettingsChanged();
}
......@@ -27,15 +27,6 @@ class App: Application() {
companion object {
const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
const val OVERRIDE_PROXY = "override_proxy"
const val OVERRIDE_PROXY_HOST = "override_proxy_host"
const val OVERRIDE_PROXY_PORT = "override_proxy_port"
const val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
const val OVERRIDE_PROXY_PORT_DEFAULT = 8118
fun getLauncherBitmap(context: Context): Bitmap? {
val drawableLogo = if (android.os.Build.VERSION.SDK_INT >= 21)
context.getDrawable(R.mipmap.ic_launcher)
......@@ -62,7 +53,7 @@ class App: Application() {
super.onCreate()
Logger.initialize(this)
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG)
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
......@@ -72,13 +63,6 @@ class App: Application() {
.penaltyLog()
.build())
// main thread
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build())
}
if (Build.VERSION.SDK_INT <= 21)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
......
......@@ -30,7 +30,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import okhttp3.HttpUrl
......@@ -293,84 +293,81 @@ class DavService: Service() {
NotificationManagerCompat.from(this)
.cancel(service.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS)
Settings.getInstance(this)?.use { settings ->
// create authenticating OkHttpClient (credentials taken from account settings)
HttpClient.Builder(this, settings, AccountSettings(this, settings, account))
.setForeground(true)
.build().use { client ->
val httpClient = client.okHttpClient
// refresh home set list (from principal)
readPrincipal()?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
queryHomeSets(httpClient, principalUrl)
}
// create authenticating OkHttpClient (credentials taken from account settings)
HttpClient.Builder(this, AccountSettings(this, account))
.setForeground(true)
.build().use { client ->
val httpClient = client.okHttpClient
// refresh home set list (from principal)
readPrincipal()?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
queryHomeSets(httpClient, principalUrl)
}
// remember selected collections
val selectedCollections = HashSet<HttpUrl>()
collections.values
.filter { it.selected }
.forEach { (url, _) -> selectedCollections += url }
// remember selected collections
val selectedCollections = HashSet<HttpUrl>()
collections.values
.filter { it.selected }
.forEach { (url, _) -> selectedCollections += url }
// now refresh collections (taken from home sets)
val itHomeSets = homeSets.iterator()
while (itHomeSets.hasNext()) {
val homeSetUrl = itHomeSets.next()
Logger.log.fine("Listing home set $homeSetUrl")
// now refresh collections (taken from home sets)
val itHomeSets = homeSets.iterator()
while (itHomeSets.hasNext()) {
val homeSetUrl = itHomeSets.next()
Logger.log.fine("Listing home set $homeSetUrl")
try {
DavResource(httpClient, homeSetUrl).propfind(1, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val info = CollectionInfo(response)
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
if ((serviceType == Services.SERVICE_CARDDAV && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)))
collections[response.href] = info
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete home set only if it was not accessible (40x)
itHomeSets.remove()
}
}
// check/refresh unconfirmed collections
val itCollections = collections.entries.iterator()
while (itCollections.hasNext()) {
val (url, info) = itCollections.next()
if (!info.confirmed)
try {
DavResource(httpClient, homeSetUrl).propfind(1, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
DavResource(httpClient, url).propfind(0, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val info = CollectionInfo(response)
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
val collectionInfo = CollectionInfo(response)
collectionInfo.confirmed = true
if ((serviceType == Services.SERVICE_CARDDAV && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)))
collections[response.href] = info
// remove unusable collections
if ((serviceType == Services.SERVICE_CARDDAV && collectionInfo.type != CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && !arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(collectionInfo.type)) ||
(collectionInfo.type == CollectionInfo.Type.WEBCAL && collectionInfo.source == null))
itCollections.remove()
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete home set only if it was not accessible (40x)
itHomeSets.remove()
// delete collection only if it was not accessible (40x)
itCollections.remove()
else
throw e
}
}
// check/refresh unconfirmed collections
val itCollections = collections.entries.iterator()
while (itCollections.hasNext()) {
val (url, info) = itCollections.next()
if (!info.confirmed)
try {
DavResource(httpClient, url).propfind(0, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val collectionInfo = CollectionInfo(response)
collectionInfo.confirmed = true
// remove unusable collections
if ((serviceType == Services.SERVICE_CARDDAV && collectionInfo.type != CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && !arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(collectionInfo.type)) ||
(collectionInfo.type == CollectionInfo.Type.WEBCAL && collectionInfo.source == null))
itCollections.remove()
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete collection only if it was not accessible (40x)
itCollections.remove()
else
throw e
}
}
// restore selections
for (url in selectedCollections)
collections[url]?.let { it.selected = true }
}
// restore selections
for (url in selectedCollections)
collections[url]?.let { it.selected = true }
}
db.beginTransactionNonExclusive()
......
......@@ -17,7 +17,8 @@ import at.bitfire.dav4android.Constants
import at.bitfire.dav4android.UrlUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
......@@ -67,7 +68,6 @@ class HttpClient private constructor(
class Builder(
val context: Context? = null,
val settings: ISettings? = null,
accountSettings: AccountSettings? = null,
val logger: java.util.logging.Logger = Logger.log
) {
......@@ -89,32 +89,36 @@ class HttpClient private constructor(
orig.addInterceptor(loggingInterceptor)
}
settings?.let {
context?.let {
val settings = Settings.getInstance(context)
// custom proxy support
try {
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
if (settings.getBoolean(Settings.OVERRIDE_PROXY) == true) {
val address = InetSocketAddress(
settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT),
settings.getInt(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
settings.getString(Settings.OVERRIDE_PROXY_HOST)
?: Settings.OVERRIDE_PROXY_HOST_DEFAULT,
settings.getInt(Settings.OVERRIDE_PROXY_PORT)
?: Settings.OVERRIDE_PROXY_PORT_DEFAULT
)
val proxy = Proxy(Proxy.Type.HTTP, address)
orig.proxy(proxy)
Logger.log.log(Level.INFO, "Using proxy", proxy)
}
} catch(e: Exception) {
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
}
context?.let {
if (BuildConfig.customCerts)
customCertManager(CustomCertManager(context, true, !settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false)))
//if (BuildConfig.customCerts)
customCertManager(CustomCertManager(context, true,
!(settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
?: Settings.DISTRUST_SYSTEM_CERTIFICATES_DEFAULT)))
}
// use account settings for authentication
accountSettings?.let {
addAuthentication(null, it.credentials())
}
}
// use account settings for authentication
accountSettings?.let {
addAuthentication(null, it.credentials())
}
}
......@@ -176,6 +180,8 @@ class HttpClient private constructor(
var keyManager: KeyManager? = null
try {
certificateAlias?.let { alias ->
val context = requireNotNull(context)
// get client certificate and private key
val certs = KeyChain.getCertificateChain(context, alias) ?: return@let
val key = KeyChain.getPrivateKey(context, alias) ?: return@let
......
......@@ -14,9 +14,9 @@ import android.content.Intent
import android.content.SharedPreferences
import android.os.Process
import android.preference.PreferenceManager
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import android.util.Log
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.AppSettingsActivity
import at.bitfire.davdroid.ui.NotificationUtils
......@@ -35,14 +35,14 @@ object Logger {
private lateinit var preferences: SharedPreferences
fun initialize(context: Context) {
preferences = PreferenceManager.getDefaultSharedPreferences(context)
fun initialize(appContext: Context) {
preferences = PreferenceManager.getDefaultSharedPreferences(appContext)
preferences.registerOnSharedPreferenceChangeListener { _, s ->
if (s == LOG_TO_EXTERNAL_STORAGE)
reinitialize(context.applicationContext)
reinitialize(appContext.applicationContext)
}
reinitialize(context.applicationContext)
reinitialize(appContext)
}
private fun reinitialize(context: Context) {
......
......@@ -14,8 +14,8 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import android.preference.PreferenceManager
import at.bitfire.davdroid.App
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.StartupDialogFragment
import java.util.logging.Level