Loading src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt +0 −1 Original line number Diff line number Diff line Loading @@ -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.* Loading src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt +80 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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)) Loading @@ -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() } } } src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt +10 −4 Original line number Diff line number Diff line Loading @@ -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.* Loading Loading @@ -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) } Loading Loading @@ -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 Loading @@ -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() Loading src/main/java/at/bitfire/ical4android/AndroidTask.kt +91 −7 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 } } Loading Loading @@ -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 -> Loading @@ -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>() Loading Loading @@ -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) Loading Loading @@ -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 -> Loading Loading @@ -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) Loading @@ -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)") Loading Loading @@ -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" } } } src/main/java/at/bitfire/ical4android/AndroidTaskList.kt +54 −3 Original line number Diff line number Diff line Loading @@ -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.* Loading @@ -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( Loading Loading @@ -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) Loading @@ -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 Loading
src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt +0 −1 Original line number Diff line number Diff line Loading @@ -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.* Loading
src/androidTest/java/at/bitfire/ical4android/AndroidTaskListTest.kt +80 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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)) Loading @@ -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() } } }
src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt +10 −4 Original line number Diff line number Diff line Loading @@ -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.* Loading Loading @@ -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) } Loading Loading @@ -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 Loading @@ -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() Loading
src/main/java/at/bitfire/ical4android/AndroidTask.kt +91 −7 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 } } Loading Loading @@ -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 -> Loading @@ -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>() Loading Loading @@ -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) Loading Loading @@ -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 -> Loading Loading @@ -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) Loading @@ -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)") Loading Loading @@ -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" } } }
src/main/java/at/bitfire/ical4android/AndroidTaskList.kt +54 −3 Original line number Diff line number Diff line Loading @@ -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.* Loading @@ -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( Loading Loading @@ -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) Loading @@ -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