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

Commit 7b5dfda1 authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Full support for RELATED-TO

parent 232477ec
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -24,7 +24,6 @@ import at.bitfire.ical4android.impl.TestEvent
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateList
import net.fortuna.ical4j.model.Dur
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.Value
import net.fortuna.ical4j.model.property.*
+80 −20
Original line number Diff line number Diff line
@@ -11,13 +11,16 @@ package at.bitfire.ical4android
import android.accounts.Account
import android.content.ContentUris
import android.content.ContentValues
import androidx.test.filters.MediumTest
import android.database.DatabaseUtils
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import at.bitfire.ical4android.impl.TestTask
import at.bitfire.ical4android.impl.TestTaskList
import net.fortuna.ical4j.model.property.RelatedTo
import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.Properties
import org.dmfs.tasks.contract.TaskContract.Property.Relation
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.*
import org.junit.Assume.assumeNotNull
import org.junit.Before
import org.junit.Test
@@ -43,24 +46,26 @@ class AndroidTaskListTest {
        provider?.close()
    }


    @MediumTest
    @Test
    fun testManageTaskLists() {
        // create task list
    private fun createTaskList(): TestTaskList {
        val info = ContentValues()
        info.put(TaskContract.TaskLists.LIST_NAME, "Test Task List")
        info.put(TaskContract.TaskLists.LIST_COLOR, 0xffff0000)
        info.put(TaskContract.TaskLists.OWNER, "test@example.com")
        info.put(TaskContract.TaskLists.SYNC_ENABLED, 1)
        info.put(TaskContract.TaskLists.VISIBLE, 1)

        val uri = AndroidTaskList.create(testAccount, provider!!, info)
        assertNotNull(uri)

        // query task list
        val taskList = AndroidTaskList.findByID(testAccount, provider!!, TestTaskList.Factory, ContentUris.parseId(uri))
        assertNotNull(taskList)
        return AndroidTaskList.findByID(testAccount, provider!!, TestTaskList.Factory, ContentUris.parseId(uri))
    }


    @Test
    fun testManageTaskLists() {
        val taskList = createTaskList()

        try {
            // sync URIs
            assertEquals("true", taskList.taskListSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER))
            assertEquals(testAccount.type, taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE))
@@ -69,9 +74,64 @@ class AndroidTaskListTest {
            assertEquals("true", taskList.tasksSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER))
            assertEquals(testAccount.type, taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE))
            assertEquals(testAccount.name, taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME))

        } finally {
            // delete task list
            assertEquals(1, taskList.delete())
        }
    }

    @Test
    fun testCommitRelations() {
        val taskList = createTaskList()
        assertTrue(taskList.useDelayedRelations)
        try {
            val parent = Task()
            parent.uid = "parent"
            parent.summary = "Parent task"
            val parentContentUri = TestTask(taskList, parent).add()

            val child = Task()
            child.uid = "child"
            child.summary = "Child task"
            child.relatedTo.add(RelatedTo(parent.uid))
            val childContentUri = TestTask(taskList, child).add()

            // there should be one DelayedRelation row
            taskList.provider.client.query(taskList.tasksPropertiesSyncUri(), null,
                    "${Properties.TASK_ID}=?", arrayOf(ContentUris.parseId(childContentUri).toString()),
                    null, null)!!.use { cursor ->
                assertEquals(1, cursor.count)
                cursor.moveToNext()

                val row = ContentValues()
                DatabaseUtils.cursorRowToContentValues(cursor, row)

                assertEquals(AndroidTask.DelayedRelation.CONTENT_ITEM_TYPE, row.getAsString(Properties.MIMETYPE))
                assertNull(row.getAsLong(Relation.RELATED_ID))
                assertEquals(parent.uid, row.getAsString(Relation.RELATED_UID))
                assertEquals(Relation.RELTYPE_PARENT, row.getAsInteger(Relation.RELATED_TYPE))
            }

            taskList.commitRelations()

            // now there must be a real Relation row
            taskList.provider.client.query(taskList.tasksPropertiesSyncUri(), null,
                    "${Properties.TASK_ID}=?", arrayOf(ContentUris.parseId(childContentUri).toString()),
                    null, null)!!.use { cursor ->
                assertEquals(1, cursor.count)
                cursor.moveToNext()

                val row = ContentValues()
                DatabaseUtils.cursorRowToContentValues(cursor, row)

                assertEquals(Relation.CONTENT_ITEM_TYPE, row.getAsString(Properties.MIMETYPE))
                assertEquals(ContentUris.parseId(parentContentUri), row.getAsLong(Relation.RELATED_ID))
                assertEquals(parent.uid, row.getAsString(Relation.RELATED_UID))
                assertEquals(Relation.RELTYPE_PARENT, row.getAsInteger(Relation.RELATED_TYPE))
            }
        } finally {
            taskList.delete()
        }
    }

}
+10 −4
Original line number Diff line number Diff line
@@ -17,10 +17,8 @@ import at.bitfire.ical4android.impl.TestTask
import at.bitfire.ical4android.impl.TestTaskList
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.TimeZone
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.Due
import net.fortuna.ical4j.model.property.Organizer
import net.fortuna.ical4j.model.property.XProperty
import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.property.*
import org.dmfs.tasks.contract.TaskContract
import org.junit.After
import org.junit.Assert.*
@@ -51,6 +49,7 @@ class AndroidTaskTest {

        taskList = TestTaskList.create(testAccount, providerOrNull)
        assertNotNull("Couldn't find/create test task list", taskList)
        taskList!!.useDelayedRelations = false

        taskListUri = ContentUris.withAppendedId(provider!!.taskListsUri(), taskList!!.id)
    }
@@ -80,6 +79,11 @@ class AndroidTaskTest {

        // extended properties
        task.categories.addAll(arrayOf("Cat1", "Cat2"))

        val sibling = RelatedTo("most-fields2@example.com")
        sibling.parameters.add(RelType.SIBLING)
        task.relatedTo.add(sibling)

        task.unknownProperties += XProperty("X-UNKNOWN-PROP", "Unknown Value")

        // add to task list
@@ -98,7 +102,9 @@ class AndroidTaskTest {
            assertEquals(task.description, task2.description)
            assertEquals(task.location, task2.location)
            assertEquals(task.dtStart, task2.dtStart)

            assertEquals(task.categories, task2.categories)
            assertEquals(task.relatedTo, task2.relatedTo)
            assertEquals(task.unknownProperties, task2.unknownProperties)
        } finally {
            testTask.delete()
+91 −7
Original line number Diff line number Diff line
@@ -10,21 +10,23 @@ package at.bitfire.ical4android

import android.content.ContentProviderOperation
import android.content.ContentProviderOperation.Builder
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.RemoteException
import at.bitfire.ical4android.AndroidTask.DelayedRelation.Companion.CONTENT_ITEM_TYPE
import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues
import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.parameter.Related
import net.fortuna.ical4j.model.property.*
import org.dmfs.tasks.contract.TaskContract.*
import org.dmfs.tasks.contract.TaskContract.Properties
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
import org.dmfs.tasks.contract.TaskContract.Property.Category
import org.dmfs.tasks.contract.TaskContract.Property.*
import java.io.FileNotFoundException
import java.net.URI
import java.net.URISyntaxException
@@ -92,6 +94,26 @@ abstract class AndroidTask(
                                populateProperty(propCursor.toValues(true))
                        }

                    // Special case: parent_id set, but no matching parent Relation row (like given by aCalendar+)
                    // In this case, we create the relation ourselves.
                    val relatedToList = task!!.relatedTo
                    values.getAsLong(Tasks.PARENT_ID)?.let { parentId ->
                        val hasParentRelation = relatedToList.any { relatedTo ->
                            val relatedType = relatedTo.getParameter(Parameter.RELTYPE)
                            relatedType == null || relatedType == RelType.PARENT
                        }
                        if (!hasParentRelation) {
                            // get UID of parent task
                            val parentContentUri = ContentUris.withAppendedId(taskList.tasksSyncUri(), parentId)
                            client.query(parentContentUri, arrayOf(Tasks._UID), null, null, null)?.use { cursor ->
                                if (cursor.moveToNext()) {
                                    // add RelatedTo for parent task
                                    relatedToList += RelatedTo(cursor.getString(0))
                                }
                            }
                        }
                    }

                    return task
                }
            }
@@ -189,6 +211,8 @@ abstract class AndroidTask(
                populateAlarm(row)
            Category.CONTENT_ITEM_TYPE ->
                task.categories += row.getAsString(Category.CATEGORY_NAME)
            Relation.CONTENT_ITEM_TYPE ->
                populateRelatedTo(row)
            UnknownProperty.CONTENT_ITEM_TYPE ->
                task.unknownProperties += UnknownProperty.fromJsonString(row.getAsString(UNKNOWN_PROPERTY_DATA))
            else ->
@@ -197,7 +221,6 @@ abstract class AndroidTask(
    }

    protected open fun populateAlarm(row: ContentValues) {
        Constants.log.log(Level.FINE, "Read task reminder from tasks provider", row)
        val task = requireNotNull(task)
        val props = PropertyList<Property>()

@@ -225,6 +248,28 @@ abstract class AndroidTask(
        task.alarms += VAlarm(props)
    }

    protected open fun populateRelatedTo(row: ContentValues) {
        val uid = row.getAsString(Relation.RELATED_UID)
        if (uid == null) {
            Constants.log.warning("Task relation doesn't refer to same task list; can't be synchronized")
            return
        }

        val relatedTo = RelatedTo(uid)

        // add relation type as reltypeparam
        relatedTo.parameters.add(when (row.getAsInteger(Relation.RELATED_TYPE)) {
            Relation.RELTYPE_CHILD ->
                RelType.CHILD
            Relation.RELTYPE_SIBLING ->
                RelType.SIBLING
            else /* Relation.RELTYPE_PARENT, default value */ ->
                RelType.PARENT
        })

        requireNotNull(task).relatedTo.add(relatedTo)
    }


    fun add(): Uri {
        val batch = BatchOperation(taskList.provider.client)
@@ -261,13 +306,14 @@ abstract class AndroidTask(
        return uri
    }

    private fun insertProperties(batch: BatchOperation) {
    protected open fun insertProperties(batch: BatchOperation) {
        insertAlarms(batch)
        insertCategories(batch)
        insertRelatedTo(batch)
        insertUnknownProperties(batch)
    }

    private fun insertAlarms(batch: BatchOperation) {
    protected open fun insertAlarms(batch: BatchOperation) {
        for (alarm in requireNotNull(task).alarms) {
            val alarmRef = when (alarm.trigger.getParameter(Parameter.RELATED)) {
                Related.END ->
@@ -300,7 +346,7 @@ abstract class AndroidTask(
        }
    }

    private fun insertCategories(batch: BatchOperation) {
    protected open fun insertCategories(batch: BatchOperation) {
        for (category in requireNotNull(task).categories) {
            val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri())
                    .withValue(Category.TASK_ID, id)
@@ -311,7 +357,32 @@ abstract class AndroidTask(
        }
    }

    private fun insertUnknownProperties(batch: BatchOperation) {
    protected open fun insertRelatedTo(batch: BatchOperation) {
        val mimeType = if (taskList.useDelayedRelations)
            DelayedRelation.CONTENT_ITEM_TYPE
        else
            Relation.CONTENT_ITEM_TYPE

        for (relatedTo in requireNotNull(task).relatedTo) {
            val relType = when ((relatedTo.getParameter(Parameter.RELTYPE) as RelType?)) {
                RelType.CHILD ->
                    Relation.RELTYPE_CHILD
                RelType.SIBLING ->
                    Relation.RELTYPE_SIBLING
                else /* RelType.PARENT, default value */ ->
                    Relation.RELTYPE_PARENT
            }
            val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri())
                    .withValue(Relation.TASK_ID, id)
                    .withValue(Relation.MIMETYPE, mimeType)
                    .withValue(Relation.RELATED_UID, relatedTo.value)
                    .withValue(Relation.RELATED_TYPE, relType)
            Constants.log.log(Level.FINE, "Inserting relation", builder.build())
            batch.enqueue(BatchOperation.Operation(builder))
        }
    }

    protected open fun insertUnknownProperties(batch: BatchOperation) {
        for (property in requireNotNull(task).unknownProperties) {
            if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) {
                Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)")
@@ -434,4 +505,17 @@ abstract class AndroidTask(

    override fun toString() = MiscUtils.reflectionToString(this)


    /**
     * A delayed relation row represents a relation which possibly can't be resolved yet.
     * Same definition as [Relation], only the row type is [CONTENT_ITEM_TYPE] instead of [Relation.CONTENT_ITEM_TYPE].
     */
    class DelayedRelation {

        companion object {
            const val CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.ical4android.delayed-relation"
        }

    }

}
+54 −3
Original line number Diff line number Diff line
@@ -9,14 +9,16 @@
package at.bitfire.ical4android

import android.accounts.Account
import android.content.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues
import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import org.dmfs.tasks.contract.TaskContract.*
import org.dmfs.tasks.contract.TaskContract.Properties
import org.dmfs.tasks.contract.TaskContract.Property.Relation
import java.io.FileNotFoundException
import java.util.*

@@ -38,8 +40,9 @@ abstract class AndroidTaskList<out T: AndroidTask>(
        /**
         * Acquires a [android.content.ContentProviderClient] for a supported task provider. If multiple providers are
         * available, a pre-defined priority list is taken into account.
         *
         * @return A [TaskProvider], or null if task storage is not available/accessible.
         *         Caller is responsible for calling release()!
         * Caller is responsible for calling [TaskProvider.close]!
         */
        fun acquireTaskProvider(context: Context): TaskProvider? {
            val byPriority = arrayOf(
@@ -96,6 +99,16 @@ abstract class AndroidTaskList<out T: AndroidTask>(
    var isSynced = false
    var isVisible = false

    /**
     * When tasks are added or updated, they may refer to related tasks ([Task.relatedTo]),
     * but these related tasks may not be available yet (for instance, because they have not been
     * synchronized yet), so that the tasks provider can't establish the relation in the database.
     *
     * When delayed relations are used, [commitRelations] must be called after
     * operations which potentially add relations (namely [AndroidTask.add] and [AndroidTask.update]).
     */
    var useDelayedRelations = true


    protected fun populate(values: ContentValues) {
        syncId = values.getAsString(TaskLists._SYNC_ID)
@@ -108,12 +121,50 @@ abstract class AndroidTaskList<out T: AndroidTask>(
    fun update(info: ContentValues) = provider.client.update(taskListSyncUri(), info, null, null)
    fun delete() = provider.client.delete(taskListSyncUri(), null, null)

    /**
     * Transforms [AndroidTask.DelayedRelation]s to real [org.dmfs.tasks.contract.TaskContract.Property.Relation]s. Only
     * useful when [useDelayedRelations] is active.
     */
    fun commitRelations() {
        Constants.log.fine("Commiting relations")

        val batch = BatchOperation(provider.client)
        provider.client.query(tasksPropertiesSyncUri(),
                arrayOf(Properties.PROPERTY_ID, Properties.TASK_ID, Relation.RELATED_TYPE, Relation.RELATED_UID),
                "${Properties.MIMETYPE}=?", arrayOf(AndroidTask.DelayedRelation.CONTENT_ITEM_TYPE), null)?.use { cursor ->
            while (cursor.moveToNext()) {
                val id = cursor.getLong(0)
                val taskId = cursor.getLong(1)
                val relatedType = cursor.getInt(2)
                val relatedUid = cursor.getString(3)

                // create new Relation row
                batch.enqueue(BatchOperation.Operation(
                        ContentProviderOperation.newInsert(tasksPropertiesSyncUri())
                                .withValue(Relation.TASK_ID, taskId)
                                .withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE)
                                .withValue(Relation.RELATED_TYPE, relatedType)
                                .withValue(Relation.RELATED_UID, relatedUid)
                ))

                // delete DelayedRelation row
                val delayedRelationUri = ContentUris.withAppendedId(tasksPropertiesSyncUri(), id)
                batch.enqueue(BatchOperation.Operation(
                        ContentProviderOperation.newDelete(delayedRelationUri)
                ))
            }
        }
        batch.commit()
    }


    /**
     * Queries tasks from this task list. Adds a WHERE clause that restricts the
     * query to [Tasks.LIST_ID] = [id].
     *
     * @param _where selection
     * @param _whereArgs arguments for selection
     *
     * @return events from this task list which match the selection
     */
    fun queryTasks(_where: String? = null, _whereArgs: Array<String>? = null): List<T> {
Loading