Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6af50dbc authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Use Room for database

parent 7a44f618
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -95,6 +95,11 @@ dependencies {
    implementation 'com.google.android:flexbox:1.1.0'
    implementation 'com.google.android.material:material:1.0.0'

    def room_version = '2.1.0-alpha06'
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

    implementation(':dav4jvm') {
        exclude group: 'org.ogce', module: 'xpp3'	// Android comes with its own XmlPullParser
    }
+199 −239
Original line number Diff line number Diff line
@@ -10,38 +10,34 @@ package at.bitfire.davdroid

import android.accounts.Account
import android.app.PendingIntent
import android.app.Service
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.os.Binder
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.room.Transaction
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.*
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.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.HomeSet
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.io.IOException
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
import kotlin.concurrent.thread

class DavService: Service() {
class DavService: android.app.Service() {

    companion object {
        const val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
@@ -52,6 +48,16 @@ class DavService: Service() {
            contents://<authority>/<account.type>/<account name>
         **/
        const val ACTION_FORCE_SYNC = "forceSync"

        val DAV_COLLECTION_PROPERTIES = arrayOf(
                ResourceType.NAME,
                CurrentUserPrivilegeSet.NAME,
                DisplayName.NAME,
                AddressbookDescription.NAME, SupportedAddressData.NAME,
                CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
                Source.NAME
        )

    }

    private val runningRefresh = HashSet<Long>()
@@ -133,54 +139,22 @@ class DavService: Service() {
        ContentResolver.requestSync(account, authority, extras)
    }

    private fun refreshCollections(service: Long) {
        OpenHelper(this@DavService).use { dbHelper ->
            val db = dbHelper.writableDatabase
    private fun refreshCollections(serviceId: Long) {
        val db = AppDatabase.getInstance(this)
        val homeSetDao = db.homeSetDao()
        val collectionDao = db.collectionDao()

            val serviceType by lazy {
                db.query(Services._TABLE, arrayOf(Services.SERVICE), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
                    if (cursor.moveToNext())
                        return@lazy cursor.getString(0)
                } ?: throw IllegalArgumentException("Service not found")
            }
        val service = db.serviceDao().getById(serviceId) ?: throw IllegalArgumentException("Service not found")
        val account = Account(service.accountName, getString(R.string.account_type))

            val account by lazy {
                db.query(Services._TABLE, arrayOf(Services.ACCOUNT_NAME), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
                    if (cursor.moveToNext())
                        return@lazy Account(cursor.getString(0), getString(R.string.account_type))
                }
                throw IllegalArgumentException("Account not found")
            }
        val oldHomeSets = homeSetDao.getByService(serviceId)
        val oldCollections = collectionDao.getByService(serviceId)

            val homeSets by lazy {
                val homeSets = mutableSetOf<HttpUrl>()
                db.query(HomeSets._TABLE, arrayOf(HomeSets.URL), "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
                    while (cursor.moveToNext())
                        HttpUrl.parse(cursor.getString(0))?.let { homeSets += it }
                }
                homeSets
            }
        val homeSets = oldHomeSets.toMutableList()

            val collections by lazy {
                val collections = mutableMapOf<HttpUrl, CollectionInfo>()
                db.query(Collections._TABLE, null, "${Collections.SERVICE_ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
                    while (cursor.moveToNext()) {
                        val values = ContentValues(cursor.columnCount)
                        DatabaseUtils.cursorRowToContentValues(cursor, values)
                        values.getAsString(Collections.URL)?.let { url ->
                            HttpUrl.parse(url)?.let { collections.put(it, CollectionInfo(values)) }
                        }
                    }
                }
                collections
            }

            fun readPrincipal(): HttpUrl? {
                db.query(Services._TABLE, arrayOf(Services.PRINCIPAL), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
                    if (cursor.moveToNext())
                        cursor.getString(0)?.let { return HttpUrl.parse(it) }
                }
                return null
        val collections = mutableMapOf<HttpUrl, Collection>()
        oldCollections.forEach {
            collections[it.url] = it
        }

        /**
@@ -191,7 +165,7 @@ class DavService: Service() {
         * @throws DavException
         */
        fun queryHomeSets(client: OkHttpClient, url: HttpUrl, recurse: Boolean = true) {
                var related = setOf<HttpUrl>()
            val related = mutableSetOf<HttpUrl>()

            fun findRelated(root: HttpUrl, dav: Response) {
                // refresh home sets: calendar-proxy-read/write-for
@@ -224,13 +198,15 @@ class DavService: Service() {
            }

            val dav = DavResource(client, url)
                when (serviceType) {
                    Services.SERVICE_CARDDAV ->
            when (service.type) {
                Service.TYPE_CARDDAV ->
                    try {
                        dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME) { response, _ ->
                            response[AddressbookHomeSet::class.java]?.let { homeSet ->
                                for (href in homeSet.hrefs)
                                        dav.location.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
                                    dav.location.resolve(href)?.let {
                                        homeSets += HomeSet(0, serviceId, UrlUtils.withTrailingSlash(it))
                                    }
                            }

                            if (recurse)
@@ -242,12 +218,14 @@ class DavService: Service() {
                        else
                            throw e
                    }
                    Services.SERVICE_CALDAV -> {
                Service.TYPE_CALDAV -> {
                    try {
                        dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) { response, _ ->
                            response[CalendarHomeSet::class.java]?.let { homeSet ->
                                for (href in homeSet.hrefs)
                                        dav.location.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(it)) }
                                    dav.location.resolve(href)?.let {
                                        homeSets += HomeSet(0, serviceId, UrlUtils.withTrailingSlash(it))
                                    }
                            }

                            if (recurse)
@@ -266,29 +244,18 @@ class DavService: Service() {
                queryHomeSets(client, resource, false)
        }

            fun saveHomeSets() {
                db.delete(HomeSets._TABLE, "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()))
                for (homeSet in homeSets) {
                    val values = ContentValues(2)
                    values.put(HomeSets.SERVICE_ID, service)
                    values.put(HomeSets.URL, homeSet.toString())
                    db.insertOrThrow(HomeSets._TABLE, null, values)
                }
            }
        @Transaction
        fun saveResults() {
            homeSetDao.deleteByService(serviceId)
            homeSetDao.insert(homeSets.onEach { it.serviceId = serviceId })

            fun saveCollections() {
                db.delete(Collections._TABLE, "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()))
                for ((_,collection) in collections) {
                    val values = collection.toDB()
                    Logger.log.log(Level.FINE, "Saving collection", values)
                    values.put(Collections.SERVICE_ID, service)
                    db.insertWithOnConflict(Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE)
                }
            collectionDao.deleteByService(serviceId)
            val records = collections.values.toList()
            collectionDao.insert(records.onEach { it.serviceId = serviceId })
        }


        try {
                Logger.log.info("Refreshing $serviceType collections of service #$service")
            Logger.log.info("Refreshing ${service.type} collections of service #$service")

            // cancel previous notification
            NotificationManagerCompat.from(this)
@@ -301,34 +268,35 @@ class DavService: Service() {
                val httpClient = client.okHttpClient

                // refresh home set list (from principal)
                    readPrincipal()?.let { principalUrl ->
                service.principal?.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 }
                collections.forEach { (url, collection) ->
                    if (collection.sync)
                        selectedCollections += url
                }

                // now refresh collections (taken from home sets)
                val itHomeSets = homeSets.iterator()
                while (itHomeSets.hasNext()) {
                        val homeSetUrl = itHomeSets.next()
                    val homeSetUrl = itHomeSets.next().url
                    Logger.log.fine("Listing home set $homeSetUrl")

                    try {
                            DavResource(httpClient, homeSetUrl).propfind(1, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
                        DavResource(httpClient, homeSetUrl).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, _ ->
                            if (!response.isSuccess())
                                return@propfind

                                val info = CollectionInfo(response)
                            val info = Collection.fromDavResponse(response) ?: return@propfind
                            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)))
                            if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) ||
                                (service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(info.type)))
                                collections[response.href] = info
                        }
                    } catch(e: HttpException) {
@@ -344,17 +312,17 @@ class DavService: Service() {
                    val (url, info) = itCollections.next()
                    if (!info.confirmed)
                        try {
                                DavResource(httpClient, url).propfind(0, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
                            DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
                                if (!response.isSuccess())
                                    return@propfind

                                    val collectionInfo = CollectionInfo(response)
                                    collectionInfo.confirmed = true
                                val collection = Collection.fromDavResponse(response) ?: return@propfind
                                collection.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))
                                if ((service.type == Service.TYPE_CARDDAV && collection.type != Collection.TYPE_ADDRESSBOOK) ||
                                    (service.type == Service.TYPE_CALDAV && !arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) ||
                                    (collection.type == Collection.TYPE_WEBCAL && collection.source == null))
                                    itCollections.remove()
                            }
                        } catch(e: HttpException) {
@@ -368,17 +336,10 @@ class DavService: Service() {

                // restore selections
                for (url in selectedCollections)
                        collections[url]?.let { it.selected = true }
                    collections[url]?.let { it.sync = true }
            }

                db.beginTransactionNonExclusive()
                try {
                    saveHomeSets()
                    saveCollections()
                    db.setTransactionSuccessful()
                } finally {
                    db.endTransaction()
                }
            saveResults()

        } catch(e: InvalidAccountException) {
            Logger.log.log(Level.SEVERE, "Invalid account", e)
@@ -398,13 +359,12 @@ class DavService: Service() {
                    .setCategory(NotificationCompat.CATEGORY_ERROR)
                    .build()
            NotificationManagerCompat.from(this)
                        .notify(service.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
                    .notify(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
        } finally {
                runningRefresh.remove(service)
            runningRefresh.remove(serviceId)
            refreshingStatusListeners.mapNotNull { it.get() }.forEach {
                    it.onDavRefreshStatusChanged(service, false)
                    it.onDavRefreshFinished(service)
                }
                it.onDavRefreshStatusChanged(serviceId, false)
                it.onDavRefreshFinished(serviceId)
            }
        }

+18 −21
Original line number Diff line number Diff line
@@ -14,48 +14,45 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.Services
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import kotlin.concurrent.thread

class PackageChangedReceiver: BroadcastReceiver() {

    companion object {

        @WorkerThread
        fun updateTaskSync(context: Context) {
            val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
            Logger.log.info("Tasks provider available = $tasksInstalled")

            // check all accounts and (de)activate OpenTasks if a CalDAV service is defined
            ServiceDB.OpenHelper(context).use { dbHelper ->
                val db = dbHelper.readableDatabase

                db.query(Services._TABLE, arrayOf(Services.ACCOUNT_NAME),
                        "${Services.SERVICE}=?", arrayOf(Services.SERVICE_CALDAV), null, null, null)?.use { cursor ->
                    while (cursor.moveToNext()) {
                        val account = Account(cursor.getString(0), context.getString(R.string.account_type))

            val db = AppDatabase.getInstance(context)
            db.serviceDao().getByType(Service.TYPE_CALDAV).forEach { service ->
                val account = Account(service.accountName, context.getString(R.string.account_type))
                if (tasksInstalled) {
                    if (ContentResolver.getIsSyncable(account, OpenTasks.authority) <= 0) {
                        ContentResolver.setIsSyncable(account, OpenTasks.authority, 1)
                        ContentResolver.addPeriodicSync(account, OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL)
                    }
                } else
                            ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0)
                    ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)

            }
        }
            }
        }

    }


    override fun onReceive(context: Context, intent: Intent) {
        thread {
            updateTaskSync(context)
        }
    }

}
+169 −0

File added.

Preview size limit exceeded, changes collapsed.

+149 −0
Original line number Diff line number Diff line
package at.bitfire.davdroid.model

import androidx.room.*
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.DavUtils
import okhttp3.HttpUrl

@Entity(tableName = "collection",
        foreignKeys = [
            ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
        ],
        indices = [
            Index("serviceId","type")
        ]
)
data class Collection(
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0,

    var serviceId: Long = 0,

    var type: String,
    var url: HttpUrl,

    var privWriteContent: Boolean = true,
    var privUnbind: Boolean = true,
    var forceReadOnly: Boolean = false,

    var displayName: String? = null,
    var description: String? = null,

    // CalDAV only
    var color: Int? = null,

    /** timezone definition (full VTIMEZONE) - not a TZID! **/
    var timezone: String? = null,

    /** whether the collection supports VEVENT; in case of calendars: null means true */
    var supportsVEVENT: Boolean? = null,

    /** whether the collection supports VTODO; in case of calendars: null means true */
    var supportsVTODO: Boolean? = null,

    /** whether the collection supports VJOURNAL; in case of calendars: null means true */
    var supportsVJOURNAL: Boolean? = null,

    /** Webcal subscription source URL */
    var source: String? = null,

    /** whether this collection has been selected for synchronization */
    var sync: Boolean = false

) {

    companion object {

        const val TYPE_ADDRESSBOOK = "ADDRESS_BOOK"
        const val TYPE_CALENDAR = "CALENDAR"
        const val TYPE_WEBCAL = "WEBCAL"

        /**
         * Generates a collection entity from a WebDAV response.
         * @param dav WebDAV response
         * @return null if the response doesn't represent a collection
         */
        fun fromDavResponse(dav: Response): Collection? {
            val url = UrlUtils.withTrailingSlash(dav.href)
            val type: String = dav[ResourceType::class.java]?.let { resourceType ->
                when {
                    resourceType.types.contains(ResourceType.ADDRESSBOOK) -> Collection.TYPE_ADDRESSBOOK
                    resourceType.types.contains(ResourceType.CALENDAR)    -> Collection.TYPE_CALENDAR
                    resourceType.types.contains(ResourceType.SUBSCRIBED)  -> Collection.TYPE_WEBCAL
                    else -> null
                }
            } ?: return null

            var privWriteContent = true
            var privUnbind = true
            dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
                privWriteContent = privilegeSet.mayWriteContent
                privUnbind = privilegeSet.mayUnbind
            }

            var displayName: String? = null
            dav[DisplayName::class.java]?.let {
                if (!it.displayName.isNullOrEmpty())
                    displayName = it.displayName
            }

            var description: String? = null
            var color: Int? = null
            var timezone: String? = null
            var supportsVEVENT: Boolean? = null
            var supportsVTODO: Boolean? = null
            var source: String? = null
            when (type) {
                Collection.TYPE_ADDRESSBOOK -> {
                    dav[AddressbookDescription::class.java]?.let { description = it.description }
                }
                Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL -> {
                    dav[CalendarDescription::class.java]?.let { description = it.description }
                    dav[CalendarColor::class.java]?.let { color = it.color }
                    dav[CalendarTimezone::class.java]?.let { timezone = it.vTimeZone }

                    if (type == Collection.TYPE_CALENDAR) {
                        supportsVEVENT = true
                        supportsVTODO = true
                        dav[SupportedCalendarComponentSet::class.java]?.let {
                            supportsVEVENT = it.supportsEvents
                            supportsVTODO = it.supportsTasks
                        }
                    } else { // Type.WEBCAL
                        dav[Source::class.java]?.let { source = it.hrefs.firstOrNull() }
                        supportsVEVENT = true
                    }
                }
            }

            return Collection(
                    type = type,
                    url = url,
                    privWriteContent = privWriteContent,
                    privUnbind = privUnbind,
                    displayName = displayName,
                    description = description,
                    color = color,
                    timezone = timezone,
                    supportsVEVENT = supportsVEVENT,
                    supportsVTODO = supportsVTODO,
                    source = source
            )
        }

    }


    // non-persistent properties
    @Ignore
    var confirmed: Boolean = false

    @Ignore
    var uiEnabled: Boolean = true


    fun title() = displayName ?: DavUtils.lastSegmentOfUrl(url)

}
 No newline at end of file
Loading