Commit d8536765 authored by Ricki Hirner's avatar Ricki Hirner 🐑

Managed DAVdroid, settings framework, theming, launcher icons, lib update

parent 9036f3e1
......@@ -17,19 +17,17 @@ android {
defaultConfig {
applicationId "at.bitfire.davdroid"
minSdkVersion 16
targetSdkVersion 25
versionCode 172
versionCode 179
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
buildConfigField "boolean", "customCerts", "true"
minSdkVersion 16
targetSdkVersion 25
}
productFlavors {
standard {
versionName "1.8.1-ose"
buildConfigField "boolean", "customCerts", "true"
versionName "1.9-beta1-ose"
}
}
......
/*
* 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 at.bitfire.davdroid.App
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import org.junit.Test
class DefaultsProviderTest {
private val provider: Provider = DefaultsProvider
@Test
fun testHas() {
assertEquals(Pair(false, true), provider.has("notExisting"))
assertEquals(Pair(true, true), provider.has(App.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))
}
@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))
}
}
\ No newline at end of file
/*
* 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.ServiceConnection
import android.support.test.InstrumentationRegistry
import android.support.test.InstrumentationRegistry.getTargetContext
import at.bitfire.davdroid.App
import junit.framework.Assert.*
import org.junit.After
import org.junit.Before
import org.junit.Test
class SettingsTest {
lateinit var settings: Settings.Stub
@Before
fun init() {
InstrumentationRegistry.getContext().isRestricted
settings = Settings.getInstance(getTargetContext())!!
}
@After
fun shutdown() {
settings.close()
}
@Test
fun testHas() {
assertFalse(settings.has("notExisting"))
// provided by DefaultsProvider
assertTrue(settings.has(App.OVERRIDE_PROXY))
}
}
\ No newline at end of file
......@@ -52,23 +52,83 @@
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<receiver
android:name=".model.Settings$ReinitSettingsReceiver"
android:exported="false"
android:process=":sync">
<service android:name=".DavService"/>
<service android:name=".settings.Settings"/>
<receiver android:name=".AccountsChangedReceiver">
<intent-filter>
<action android:name="at.bitfire.davdroid.REINIT_SETTINGS"/>
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED"/>
</intent-filter>
</receiver>
<receiver
android:name=".AccountSettings$AppUpdatedReceiver"
android:exported="true">
<receiver android:name=".PackageChangedReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" android:path="at.bitfire.davdroid" />
<action android:name="android.intent.action.PACKAGE_ADDED"/>
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED"/>
<data android:scheme="package"/>
</intent-filter>
</receiver>
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ui.AboutActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"
android:parentActivityName=".ui.AccountsActivity"/>
<activity
android:name=".ui.AppSettingsActivity"
android:label="@string/app_settings"
android:parentActivityName=".ui.AccountsActivity"/>
<activity android:name=".ui.PermissionsActivity"
android:label="@string/permissions_title"
android:parentActivityName=".ui.AccountsActivity"/>
<activity
android:name=".ui.setup.LoginActivity"
android:label="@string/login_title"
android:parentActivityName=".ui.AccountsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity
android:name=".ui.AccountActivity"
android:parentActivityName=".ui.AccountsActivity">
</activity>
<activity android:name=".ui.AccountSettingsActivity"/>
<activity android:name=".ui.CreateAddressBookActivity"
android:label="@string/create_addressbook"/>
<activity android:name=".ui.CreateCalendarActivity"
android:label="@string/create_calendar"/>
<activity
android:name=".ui.DebugInfoActivity"
android:parentActivityName=".ui.AppSettingsActivity"
android:exported="true"
android:label="@string/debug_info_title">
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="@string/authority_log_provider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_paths" />
</provider>
<!-- account type "DAVdroid" -->
<service
android:name=".syncadapter.AccountAuthenticatorService"
......@@ -94,7 +154,6 @@
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars"/>
</service>
<service
android:name=".syncadapter.TasksSyncAdapterService"
android:exported="true"
......@@ -157,85 +216,6 @@
android:resource="@xml/contacts"/>
</service>
<service
android:name=".DavService"
android:enabled="true">
</service>
<receiver android:name=".AccountsChangedReceiver">
<intent-filter>
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED"/>
</intent-filter>
</receiver>
<receiver android:name=".PackageChangedReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED"/>
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED"/>
<data android:scheme="package"/>
</intent-filter>
</receiver>
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ui.AboutActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"
android:parentActivityName=".ui.AccountsActivity"/>
<activity
android:name=".ui.AppSettingsActivity"
android:label="@string/app_settings"
android:parentActivityName=".ui.AccountsActivity"/>
<activity android:name=".ui.PermissionsActivity"
android:label="@string/permissions_title"
android:parentActivityName=".ui.AccountsActivity"/>
<activity
android:name=".ui.setup.LoginActivity"
android:label="@string/login_title"
android:parentActivityName=".ui.AccountsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity
android:name=".ui.AccountActivity"
android:parentActivityName=".ui.AccountsActivity">
</activity>
<activity android:name=".ui.AccountSettingsActivity"/>
<activity android:name=".ui.CreateAddressBookActivity"
android:label="@string/create_addressbook"/>
<activity android:name=".ui.CreateCalendarActivity"
android:label="@string/create_calendar"/>
<activity
android:name=".ui.DebugInfoActivity"
android:exported="true"
android:label="@string/debug_info_title">
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="@string/authority_log_provider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_paths" />
</provider>
</application>
</manifest>
package at.bitfire.davdroid.settings;
import at.bitfire.davdroid.settings.ISettingsObserver;
interface ISettings {
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();
}
......@@ -9,8 +9,10 @@ package at.bitfire.davdroid
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.*
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcel
......@@ -25,6 +27,7 @@ import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.CalendarStorageException
......@@ -37,6 +40,7 @@ import java.util.logging.Level
class AccountSettings @Throws(InvalidAccountException::class) constructor(
val context: Context,
val settings: ISettings,
val account: Account
) {
......@@ -135,12 +139,17 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
}
}
fun getSyncWifiOnly() = accountManager.getUserData(account, KEY_WIFI_ONLY) != null
fun getSyncWifiOnly() = if (settings.has(KEY_WIFI_ONLY))
settings.getBoolean(KEY_WIFI_ONLY, false)
else
accountManager.getUserData(account, KEY_WIFI_ONLY) != null
fun setSyncWiFiOnly(wiFiOnly: Boolean) =
accountManager.setUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null)
fun getSyncWifiOnlySSIDs(): List<String>? =
accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS)?.split(',')
fun getSyncWifiOnlySSIDs(): List<String>? = (if (settings.has(KEY_WIFI_ONLY_SSIDS))
settings.getString(KEY_WIFI_ONLY_SSIDS, null)
else
accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS))?.split(',')
fun setSyncWifiOnlySSIDs(ssids: List<String>?) =
accountManager.setUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(",")))
......@@ -159,27 +168,36 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
fun setTimeRangePastDays(days: Int?) =
accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString())
fun getManageCalendarColors() = accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null
fun getManageCalendarColors() = if (settings.has(KEY_MANAGE_CALENDAR_COLORS))
settings.getBoolean(KEY_MANAGE_CALENDAR_COLORS, false)
else
accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null
fun setManageCalendarColors(manage: Boolean) =
accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0")
fun getEventColors() = accountManager.getUserData(account, KEY_EVENT_COLORS) != null
fun getEventColors() = if (settings.has(KEY_EVENT_COLORS))
settings.getBoolean(KEY_EVENT_COLORS, false)
else
accountManager.getUserData(account, KEY_EVENT_COLORS) != null
fun setEventColors(useColors: Boolean) =
accountManager.setUserData(account, KEY_EVENT_COLORS, if (useColors) "1" else null)
// CardDAV settings
fun getGroupMethod(): GroupMethod {
val name = accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD)
return if (name != null)
GroupMethod.valueOf(name)
else
GroupMethod.GROUP_VCARDS
val name = settings.getString(KEY_CONTACT_GROUP_METHOD, null) ?:
accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD)
if (name != null)
try {
return GroupMethod.valueOf(name)
}
catch (e: IllegalArgumentException) {
}
return GroupMethod.GROUP_VCARDS
}
fun setGroupMethod(method: GroupMethod) {
val name = if (method == GroupMethod.GROUP_VCARDS) null else method.name
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name)
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, method.name)
}
......@@ -464,24 +482,4 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
}
}
class AppUpdatedReceiver: BroadcastReceiver() {
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
override fun onReceive(context: Context, intent: Intent?) {
Logger.log.info("DAVdroid was updated, checking for AccountSettings version")
// peek into AccountSettings to initiate a possible migration
val accountManager = AccountManager.get(context)
for (account in accountManager.getAccountsByType(context.getString(R.string.account_type)))
try {
Logger.log.info("Checking account ${account.name}")
AccountSettings(context, account)
} catch(e: InvalidAccountException) {
Logger.log.log(Level.SEVERE, "Couldn't check for updated account settings", e)
}
}
}
}
......@@ -18,11 +18,10 @@ class App: Application() {
companion object {
@JvmField val DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts"
@JvmField val LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage"
@JvmField val OVERRIDE_PROXY = "overrideProxy"
@JvmField val OVERRIDE_PROXY_HOST = "overrideProxyHost"
@JvmField val OVERRIDE_PROXY_PORT = "overrideProxyPort"
@JvmField val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
@JvmField val OVERRIDE_PROXY = "override_proxy"
@JvmField val OVERRIDE_PROXY_HOST = "override_proxy_host"
@JvmField val OVERRIDE_PROXY_PORT = "override_proxy_port"
@JvmField val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
@JvmField val OVERRIDE_PROXY_PORT_DEFAULT = 8118
......@@ -46,7 +45,7 @@ class App: Application() {
override fun onCreate() {
super.onCreate()
Logger.reinitLogger(this)
Logger.initialize(this)
}
}
......@@ -15,14 +15,15 @@ import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4android.BasicDigestAuthHandler
import at.bitfire.dav4android.UrlUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.Settings
import at.bitfire.davdroid.settings.Settings
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.internal.tls.OkHostnameVerifier
import okhttp3.logging.HttpLoggingInterceptor
import java.io.Closeable
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import java.text.SimpleDateFormat
......@@ -42,6 +43,7 @@ class HttpClient private constructor(
class Builder @JvmOverloads constructor(
val context: Context? = null,
account: Account? = null,
accountSettings: AccountSettings? = null,
logger: java.util.logging.Logger = Logger.log
) {
......@@ -73,9 +75,7 @@ class HttpClient private constructor(
}
context?.let {
ServiceDB.OpenHelper(context).use { dbHelper ->
val settings = Settings(dbHelper.readableDatabase)
Settings.getInstance(context)?.use { settings ->
// custom proxy support
try {
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
......@@ -92,28 +92,43 @@ class HttpClient private constructor(
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
}
// use cert4android to manage self-signed certificates
if (BuildConfig.customCerts)
customCertManager(CustomCertManager(context, !settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false)))
}
}
// use account settings for authentication
accountSettings?.let {
val userName = it.username()
val password = it.password()
if (userName != null && password != null)
addAuthentication(null, userName, password)
// use account settings for authentication
val accountSettings = accountSettings ?: account?.let { AccountSettings(context, settings, it) }
accountSettings?.let {
val userName = accountSettings.username()
val password = accountSettings.password()
if (userName != null && password != null)
addAuthentication(null, userName, password)
}
}
}
}
constructor(context: Context, account: Account): this(context, AccountSettings(context, account))
constructor(context: Context, host: String?, username: String, password: String): this(context) {
addAuthentication(host, username, password)
}
fun followRedirects(follow: Boolean) { orig.followRedirects(follow) }
fun withDiskCache(): Builder {
val context = context ?: throw IllegalArgumentException("Context is required to find the cache directory")
for (dir in arrayOf(context.externalCacheDir, context.cacheDir)) {
if (dir.exists() && dir.canWrite()) {
val cacheDir = File(dir, "HttpClient")
cacheDir.mkdir()
Logger.log.fine("Using disk cache: $cacheDir")
orig.cache(Cache(cacheDir, 10*1024*1024))
break
}
}
return this
}
fun followRedirects(follow: Boolean): Builder {
orig.followRedirects(follow)
return this
}
fun customCertManager(manager: CustomCertManager) {
certManager = manager
......
......@@ -11,91 +11,102 @@ package at.bitfire.davdroid.log
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Process
import android.preference.PreferenceManager
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import android.util.Log
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.Settings
import at.bitfire.davdroid.ui.AppSettingsActivity
import org.apache.commons.lang3.time.DateFormatUtils
import java.io.File
import java.io.IOException
import java.util.logging.FileHandler
import java.util.logging.Level
import java.util.logging.Logger
object Logger {
val LOG_TO_EXTERNAL_STORAGE = "log_to_external_storage"
@JvmField
val log = Logger.getLogger("davdroid")!!
fun reinitLogger(context: Context) {
ServiceDB.OpenHelper(context).use { dbHelper ->
val settings = Settings(dbHelper.readableDatabase)
val logToFile = settings.getBoolean(App.LOG_TO_EXTERNAL_STORAGE, false)
val logVerbose = logToFile || Log.isLoggable(log.name, Log.DEBUG)
log.info("Verbose logging: $logVerbose")
// set logging level according to preferences
val rootLogger = Logger.getLogger("")
rootLogger.level = if (logVerbose) Level.ALL else Level.INFO
// remove all handlers and add our own logcat handler
rootLogger.useParentHandlers = false
rootLogger.handlers.forEach { rootLogger.removeHandler(it) }
rootLogger.addHandler(LogcatHandler)
val nm = NotificationManagerCompat.from(context)
// log to external file according to preferences
if (logToFile) {
val builder = NotificationCompat.Builder(context)
builder .setSmallIcon(R.drawable.ic_sd_storage_light)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(context.getString(R.string.logging_davdroid_file_logging))
.setLocalOnly(true)
val dir = context.getExternalFilesDir(null)
if (dir != null)
try {
val fileName = File(dir, "davdroid-${Process.myPid()}-${DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd-HHmmss")}.txt").toString()
log.info("Logging to $fileName")
val fileHandler = FileHandler(fileName)
fileHandler.formatter = PlainTextFormatter.DEFAULT
rootLogger.addHandler(fileHandler)
val prefIntent = Intent(context, AppSettingsActivity::class.java)
prefIntent.putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, "log_to_external_storage")
builder .setContentText(dir.path)
.setSubText(context.getString(R.string.logging_to_external_storage_warning))
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setStyle(NotificationCompat.BigTextStyle()
.bigText(context.getString(R.string.logging_to_external_storage, dir.path)))
.setOngoing(true)
} catch(e: IOException) {
log.log(Level.SEVERE, "Couldn't create external log file", e)
builder .setContentText(context.getString(R.string.logging_couldnt_create_file, e.localizedMessage))
.setCategory(NotificationCompat.CATEGORY_ERROR)
}
else
builder.setContentText(context.getString(R.string.logging_no_external_storage))
nm.notify(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING, builder.build())
} else
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING)
val log = java.util.logging.Logger.getLogger("davdroid")!!
lateinit var context: Context
lateinit var preferences: SharedPreferences
fun initialize(context: Context) {
this.context = context
preferences = PreferenceManager.getDefaultSharedPreferences(context)
preferences.registerOnSharedPreferenceChangeListener { _, s ->
if (s == LOG_TO_EXTERNAL_STORAGE)
reinitialize()
}
reinitialize()
}
private fun reinitialize() {