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

Commit 95898395 authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Tasks: directly use Relation rows, no need for DelayedRelation anymore

parent 7b5dfda1
Loading
Loading
Loading
Loading
+24 −22
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ 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.dmfs.tasks.contract.TaskContract.Tasks
import org.junit.After
import org.junit.Assert.*
import org.junit.Assume.assumeNotNull
@@ -81,24 +82,27 @@ class AndroidTaskListTest {
    }

    @Test
    fun testCommitRelations() {
    fun testTouchRelations() {
        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))

            // insert child before parent
            val childContentUri = TestTask(taskList, child).add()
            val childId = ContentUris.parseId(childContentUri)
            val parentContentUri = TestTask(taskList, parent).add()
            val parentId = ContentUris.parseId(parentContentUri)

            // there should be one DelayedRelation row
            // OpenTasks should provide the correct relation
            taskList.provider.client.query(taskList.tasksPropertiesSyncUri(), null,
                    "${Properties.TASK_ID}=?", arrayOf(ContentUris.parseId(childContentUri).toString()),
                    "${Properties.TASK_ID}=?", arrayOf(childId.toString()),
                    null, null)!!.use { cursor ->
                assertEquals(1, cursor.count)
                cursor.moveToNext()
@@ -106,28 +110,26 @@ class AndroidTaskListTest {
                val row = ContentValues()
                DatabaseUtils.cursorRowToContentValues(cursor, row)

                assertEquals(AndroidTask.DelayedRelation.CONTENT_ITEM_TYPE, row.getAsString(Properties.MIMETYPE))
                assertNull(row.getAsLong(Relation.RELATED_ID))
                assertEquals(Relation.CONTENT_ITEM_TYPE, row.getAsString(Properties.MIMETYPE))
                assertEquals(parentId, row.getAsLong(Relation.RELATED_ID))
                assertEquals(parent.uid, row.getAsString(Relation.RELATED_UID))
                assertEquals(Relation.RELTYPE_PARENT, row.getAsInteger(Relation.RELATED_TYPE))
            }
            // … BUT the parent_id is not updated (https://github.com/dmfs/opentasks/issues/877)
            taskList.provider.client.query(childContentUri, arrayOf(Tasks.PARENT_ID),
                    null, null, null)!!.use { cursor ->
                assertTrue(cursor.moveToNext())
                assertTrue(cursor.isNull(0))
            }

            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)
            // touch the relations to update parent_id values
            taskList.touchRelations()

                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))
            // now parent_id should bet set
            taskList.provider.client.query(childContentUri, arrayOf(Tasks.PARENT_ID),
                    null, null, null)!!.use { cursor ->
                assertTrue(cursor.moveToNext())
                assertEquals(parentId, cursor.getLong(0))
            }
        } finally {
            taskList.delete()
+0 −1
Original line number Diff line number Diff line
@@ -49,7 +49,6 @@ 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)
    }
+13 −36
Original line number Diff line number Diff line
@@ -10,12 +10,10 @@ 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
@@ -79,28 +77,28 @@ abstract class AndroidTask(

            task = Task()
            val client = taskList.provider.client
            client.query(taskSyncURI(), null, null, null, null)?.use { cursor ->
            client.query(taskSyncURI(true), null, null, null, null)?.use { cursor ->
                if (cursor.moveToFirst()) {
                    val values = cursor.toValues(true)
                    Constants.log.log(Level.FINER, "Found task", values)
                    populateTask(values)

                    if (values.getAsInteger(Tasks.HAS_PROPERTIES) != 0)
                        // fetch properties
                        client.query(taskList.tasksPropertiesSyncUri(), null,
                                "${Properties.TASK_ID}=?", arrayOf(id.toString()),
                                null)?.use { propCursor ->
                            while (propCursor.moveToNext())
                                populateProperty(propCursor.toValues(true))
                    if (values.containsKey(Properties.PROPERTY_ID)) {
                        // process the first property, which is combined with the task row
                        populateProperty(values)

                        while (cursor.moveToNext()) {
                            // process the other properties
                            populateProperty(cursor.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
                            relatedType == RelType.PARENT || relatedType == null /* RelType.PARENT is the default value */
                        }
                        if (!hasParentRelation) {
                            // get UID of parent task
@@ -358,11 +356,6 @@ abstract class AndroidTask(
    }

    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 ->
@@ -374,7 +367,7 @@ abstract class AndroidTask(
            }
            val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri())
                    .withValue(Relation.TASK_ID, id)
                    .withValue(Relation.MIMETYPE, mimeType)
                    .withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE)
                    .withValue(Relation.RELATED_UID, relatedTo.value)
                    .withValue(Relation.RELATED_TYPE, relType)
            Constants.log.log(Level.FINE, "Inserting relation", builder.build())
@@ -494,28 +487,12 @@ abstract class AndroidTask(
        return tz ?: TimeZone.getDefault()
    }

    protected fun taskSyncURI(): Uri {
    protected fun taskSyncURI(loadProperties: Boolean = false): Uri {
        val id = requireNotNull(id)
        val builder = taskList.tasksSyncUri().buildUpon()
        return ContentUris.appendId(builder, id)
                .appendQueryParameter(LOAD_PROPERTIES, "1")
                .build()
        return ContentUris.withAppendedId(taskList.tasksSyncUri(loadProperties), id)
    }


    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"
        }

    }

}
+44 −42
Original line number Diff line number Diff line
@@ -16,9 +16,9 @@ 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.*
import org.dmfs.tasks.contract.TaskContract.Properties
import org.dmfs.tasks.contract.TaskContract.Property.Relation
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.util.*

@@ -99,16 +99,6 @@ 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)
@@ -122,39 +112,43 @@ abstract class AndroidTaskList<out T: AndroidTask>(
    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.
     * When tasks are added or updated, they may refer to related tasks by UID ([Relation.RELATED_UID]).
     * However, those related tasks may not be available (for instance, because they have not been
     * synchronized yet), so that the tasks provider can't establish the actual relation (= set
     * [Relation.TASK_ID]) in the database.
     *
     * As soon as such a related task is added, OpenTasks updates the [Relation.RELATED_ID],
     * but it does *not* update [Tasks.PARENT_ID] of the parent task:
     * https://github.com/dmfs/opentasks/issues/877
     *
     * This method shall be called after all tasks have been synchronized. It touches
     *
     *   - all [Relation] rows
     *   - with [Relation.RELATED_ID] (→ related task is already synchronized)
     *   - of tasks without [Tasks.PARENT_ID] (→ only touch relevant rows)
     *
     * so that missing [Tasks.PARENT_ID] fields are updated.
     *
     * @return number of touched [Relation] rows
    */
    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 ->
    fun touchRelations(): Int {
        Constants.log.fine("Touching relations to set parent_id")
        val batchOperation = BatchOperation(provider.client)
        provider.client.query(tasksSyncUri(true), null,
                "${Tasks.LIST_ID}=? AND ${Tasks.PARENT_ID} IS NULL AND ${Relation.MIMETYPE}=? AND ${Relation.RELATED_ID} IS NOT NULL",
                arrayOf(id.toString(), Relation.CONTENT_ITEM_TYPE),
                null, 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)
                val values = cursor.toValues()
                val id = values.getAsLong(Relation.PROPERTY_ID)
                val propertyContentUri = ContentUris.withAppendedId(tasksPropertiesSyncUri(), id)
                batchOperation.enqueue(BatchOperation.Operation(
                        ContentProviderOperation.newUpdate(propertyContentUri)
                                .withValue(Relation.RELATED_ID, values.getAsLong(Relation.RELATED_ID))
                ))
            }
        }
        batch.commit()
        return batchOperation.commit()
    }


@@ -187,7 +181,15 @@ abstract class AndroidTaskList<out T: AndroidTask>(


    fun taskListSyncUri() = TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.taskListsUri(), id), account)
    fun tasksSyncUri() = TaskProvider.syncAdapterUri(provider.tasksUri(), account)
    fun tasksSyncUri(loadProperties: Boolean = false): Uri {
        val uri = TaskProvider.syncAdapterUri(provider.tasksUri(), account)
        return if (loadProperties)
            uri     .buildUpon()
                    .appendQueryParameter(TaskContract.LOAD_PROPERTIES, "1")
                    .build()
        else
            uri
    }
    fun tasksPropertiesSyncUri() = TaskProvider.syncAdapterUri(provider.propertiesUri(), account)

}