From 4564d287605bd1da4c5438d4e8474a9e7722cb6c Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Mon, 19 Jan 2026 13:42:46 +0100 Subject: [PATCH 1/8] Move logic from DmfsTaskList companion object to DmfsTaskListProvider --- .../bitfire/ical4android/DmfsTaskListTest.kt | 113 -------- .../at/bitfire/ical4android/DmfsTaskTest.kt | 1 + .../bitfire/ical4android/impl/TestTaskList.kt | 2 +- .../storage/TasksBatchOperationTest.kt | 1 + .../{ => tasks}/ContactsBatchOperationTest.kt | 6 +- .../storage/tasks/DmfsTaskListTest.kt | 155 +++++++++++ .../at/bitfire/ical4android/DmfsTask.kt | 31 ++- .../at/bitfire/ical4android/DmfsTaskList.kt | 257 ------------------ .../synctools/storage/tasks/DmfsTaskList.kt | 195 +++++++++++++ .../storage/tasks/DmfsTaskListProvider.kt | 189 +++++++++++++ .../{ => tasks}/TasksBatchOperation.kt | 5 +- 11 files changed, 565 insertions(+), 390 deletions(-) delete mode 100644 lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt rename lib/src/androidTest/kotlin/at/bitfire/synctools/storage/{ => tasks}/ContactsBatchOperationTest.kt (92%) create mode 100644 lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt delete mode 100644 lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt rename lib/src/main/kotlin/at/bitfire/synctools/storage/{ => tasks}/TasksBatchOperation.kt (79%) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt deleted file mode 100644 index 12580e4b..00000000 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package at.bitfire.ical4android - -import android.accounts.Account -import android.content.ContentUris -import android.content.ContentValues -import android.database.DatabaseUtils -import android.provider.CalendarContract -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.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Test - -class DmfsTaskListTest(providerName: TaskProvider.ProviderName): - DmfsStyleProvidersTaskTest(providerName) { - - private val testAccount = Account(javaClass.name, CalendarContract.ACCOUNT_TYPE_LOCAL) - - - private fun createTaskList(): DmfsTaskList { - 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 = DmfsTaskList.create(testAccount, provider.client, providerName, info) - assertNotNull(uri) - - return DmfsTaskList.findByID(testAccount, provider.client, providerName, 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)) - assertEquals(testAccount.name, taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME)) - - 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 - assertTrue(taskList.delete()) - } - } - - @Test - fun testTouchRelations() { - val taskList = createTaskList() - try { - val parent = Task() - parent.uid = "parent" - parent.summary = "Parent task" - - val child = Task() - child.uid = "child" - child.summary = "Child task" - child.relatedTo.add(RelatedTo(parent.uid)) - - // insert child before parent - val childContentUri = DmfsTask(taskList, child, "452a5672-e2b0-434e-92b4-bc70a7a51ef2", null, 0).add() - val childId = ContentUris.parseId(childContentUri) - val parentContentUri = DmfsTask(taskList, parent, "452a5672-e2b0-434e-92b4-bc70a7a51ef2", null, 0).add() - val parentId = ContentUris.parseId(parentContentUri) - - // OpenTasks should provide the correct relation - taskList.provider.query(taskList.tasksPropertiesSyncUri(), null, - "${Properties.TASK_ID}=?", arrayOf(childId.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(parentId, row.getAsLong(Relation.RELATED_ID)) - assertEquals(parent.uid, row.getAsString(Relation.RELATED_UID)) - assertEquals(Relation.RELTYPE_PARENT, row.getAsInteger(Relation.RELATED_TYPE)) - } - - // touch the relations to update parent_id values - taskList.touchRelations() - - // now parent_id should bet set - taskList.provider.query(childContentUri, arrayOf(Tasks.PARENT_ID), - null, null, null)!!.use { cursor -> - assertTrue(cursor.moveToNext()) - assertEquals(parentId, cursor.getLong(0)) - } - } finally { - taskList.delete() - } - } - -} diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index 8eacd3d7..816b17b9 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -15,6 +15,7 @@ import android.provider.CalendarContract import androidx.core.content.contentValuesOf import at.bitfire.ical4android.impl.TestTaskList import at.bitfire.synctools.storage.LocalStorageException +import at.bitfire.synctools.storage.tasks.DmfsTaskList import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt index 64faeb86..15ec5403 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt @@ -9,7 +9,7 @@ package at.bitfire.ical4android.impl import android.accounts.Account import android.content.ContentUris import android.content.ContentValues -import at.bitfire.ical4android.DmfsTaskList +import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.ical4android.TaskProvider import org.dmfs.tasks.contract.TaskContract diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/TasksBatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/TasksBatchOperationTest.kt index 440e1233..1668e7ce 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/TasksBatchOperationTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/TasksBatchOperationTest.kt @@ -10,6 +10,7 @@ import android.accounts.Account import at.bitfire.ical4android.DmfsStyleProvidersTaskTest import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.impl.TestTaskList +import at.bitfire.synctools.storage.tasks.TasksBatchOperation import at.bitfire.synctools.test.BuildConfig import org.dmfs.tasks.contract.TaskContract import org.junit.Test diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/ContactsBatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/ContactsBatchOperationTest.kt similarity index 92% rename from lib/src/androidTest/kotlin/at/bitfire/synctools/storage/ContactsBatchOperationTest.kt rename to lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/ContactsBatchOperationTest.kt index 306b0aef..e5c5faa4 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/ContactsBatchOperationTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/ContactsBatchOperationTest.kt @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package at.bitfire.synctools.storage +package at.bitfire.synctools.storage.tasks import android.Manifest import android.accounts.Account @@ -13,7 +13,9 @@ import android.provider.ContactsContract import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.util.MiscUtils.closeCompat -import at.bitfire.synctools.test.BuildConfig +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.ContactsBatchOperation +import at.bitfire.synctools.storage.LocalStorageException import org.junit.After import org.junit.Before import org.junit.Rule diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt new file mode 100644 index 00000000..4eb46f61 --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt @@ -0,0 +1,155 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.storage.tasks + +import android.accounts.Account +import android.content.ContentUris +import android.content.ContentValues +import android.database.DatabaseUtils +import android.provider.CalendarContract +import at.bitfire.ical4android.DmfsStyleProvidersTaskTest +import at.bitfire.ical4android.DmfsTask +import at.bitfire.ical4android.Task +import at.bitfire.ical4android.TaskProvider +import net.fortuna.ical4j.model.property.RelatedTo +import org.dmfs.tasks.contract.TaskContract +import org.junit.Assert +import org.junit.Test + +class DmfsTaskListTest(providerName: TaskProvider.ProviderName): + DmfsStyleProvidersTaskTest(providerName) { + + private val testAccount = Account(javaClass.name, CalendarContract.ACCOUNT_TYPE_LOCAL) + + private fun createTaskList(): DmfsTaskList { + 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 dmfsTaskListProvider = DmfsTaskListProvider(testAccount, provider.client, providerName) + val uri = dmfsTaskListProvider.createTaskList(testAccount, provider.client, providerName, info) + Assert.assertNotNull(uri) + + dmfsTaskListProvider.createTaskList() + + return DmfsTaskList.findByID(testAccount, provider.client, providerName, ContentUris.parseId(uri)) + } + + @Test + fun testManageTaskLists() { + val taskList = createTaskList() + + try { + // sync URIs + Assert.assertEquals( + "true", + taskList.taskListSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) + ) + Assert.assertEquals( + testAccount.type, + taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) + ) + Assert.assertEquals( + testAccount.name, + taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME) + ) + + Assert.assertEquals( + "true", + taskList.tasksSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) + ) + Assert.assertEquals( + testAccount.type, + taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) + ) + Assert.assertEquals( + testAccount.name, + taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME) + ) + } finally { + // delete task list + Assert.assertTrue(taskList.delete()) + } + } + + @Test + fun testTouchRelations() { + val taskList = createTaskList() + try { + val parent = Task() + parent.uid = "parent" + parent.summary = "Parent task" + + val child = Task() + child.uid = "child" + child.summary = "Child task" + child.relatedTo.add(RelatedTo(parent.uid)) + + // insert child before parent + val childContentUri = DmfsTask( + taskList, + child, + "452a5672-e2b0-434e-92b4-bc70a7a51ef2", + null, + 0 + ).add() + val childId = ContentUris.parseId(childContentUri) + val parentContentUri = DmfsTask( + taskList, + parent, + "452a5672-e2b0-434e-92b4-bc70a7a51ef2", + null, + 0 + ).add() + val parentId = ContentUris.parseId(parentContentUri) + + // OpenTasks should provide the correct relation + taskList.provider.client.query(taskList.tasksPropertiesSyncUri(), null, + "${TaskContract.Properties.TASK_ID}=?", arrayOf(childId.toString()), + null, null)!!.use { cursor -> + Assert.assertEquals(1, cursor.count) + cursor.moveToNext() + + val row = ContentValues() + DatabaseUtils.cursorRowToContentValues(cursor, row) + + Assert.assertEquals( + TaskContract.Property.Relation.CONTENT_ITEM_TYPE, + row.getAsString(TaskContract.Properties.MIMETYPE) + ) + Assert.assertEquals( + parentId, + row.getAsLong(TaskContract.Property.Relation.RELATED_ID) + ) + Assert.assertEquals( + parent.uid, + row.getAsString(TaskContract.Property.Relation.RELATED_UID) + ) + Assert.assertEquals( + TaskContract.Property.Relation.RELTYPE_PARENT, + row.getAsInteger(TaskContract.Property.Relation.RELATED_TYPE) + ) + } + + // touch the relations to update parent_id values + taskList.touchRelations() + + // now parent_id should bet set + taskList.provider.client.query(childContentUri, arrayOf(TaskContract.Tasks.PARENT_ID), + null, null, null)!!.use { cursor -> + Assert.assertTrue(cursor.moveToNext()) + Assert.assertEquals(parentId, cursor.getLong(0)) + } + } finally { + taskList.delete() + } + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index a8d1ac46..c07fd5d7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -12,7 +12,8 @@ import android.net.Uri import android.os.RemoteException import at.bitfire.synctools.storage.BatchOperation.CpoBuilder import at.bitfire.synctools.storage.LocalStorageException -import at.bitfire.synctools.storage.TasksBatchOperation +import at.bitfire.synctools.storage.tasks.TasksBatchOperation +import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.synctools.storage.toContentValues import at.bitfire.synctools.util.AndroidTimeUtils import net.fortuna.ical4j.model.Date @@ -106,7 +107,7 @@ class DmfsTask( val id = requireNotNull(id) try { - val client = taskList.provider + val client = taskList.provider.client client.query(taskSyncURI(true), null, null, null, null)?.use { cursor -> if (cursor.moveToFirst()) { // create new Task which will be populated @@ -136,7 +137,7 @@ class DmfsTask( } if (!hasParentRelation) { // get UID of parent task - val parentContentUri = ContentUris.withAppendedId(taskList.tasksSyncUri(), parentId) + val parentContentUri = ContentUris.withAppendedId(taskList.tasksUri(), parentId) client.query(parentContentUri, arrayOf(Tasks._UID), null, null, null)?.use { cursor -> if (cursor.moveToNext()) { // add RelatedTo for parent task @@ -339,9 +340,9 @@ class DmfsTask( fun add(): Uri { - val batch = TasksBatchOperation(taskList.provider) + val batch = TasksBatchOperation(taskList.provider.client) - val builder = CpoBuilder.newInsert(taskList.tasksSyncUri()) + val builder = CpoBuilder.newInsert(taskList.tasksUri()) buildTask(builder, false) val idxTask = batch.nextBackrefIdx() batch += builder @@ -360,11 +361,11 @@ class DmfsTask( this.task = task val existingId = requireNotNull(id) - val batch = TasksBatchOperation(taskList.provider) + val batch = TasksBatchOperation(taskList.provider.client) // remove associated rows which are added later again batch += CpoBuilder - .newDelete(taskList.tasksPropertiesSyncUri()) + .newDelete(taskList.tasksPropertiesUri()) .withSelection("${Properties.TASK_ID}=?", arrayOf(existingId.toString())) // update task @@ -381,7 +382,7 @@ class DmfsTask( } fun update(values: ContentValues) { - taskList.provider.update(taskSyncURI(), values, null, null) + taskList.provider.client.update(taskSyncURI(), values, null, null) } private fun insertProperties(batch: TasksBatchOperation, idxTask: Int?) { @@ -421,7 +422,7 @@ class DmfsTask( } val builder = CpoBuilder - .newInsert(taskList.tasksPropertiesSyncUri()) + .newInsert(taskList.tasksPropertiesUri()) .withTaskId(Alarm.TASK_ID, idxTask) .withValue(Alarm.MIMETYPE, Alarm.CONTENT_ITEM_TYPE) .withValue(Alarm.MINUTES_BEFORE, minutes) @@ -436,7 +437,7 @@ class DmfsTask( private fun insertCategories(batch: TasksBatchOperation, idxTask: Int?) { for (category in requireNotNull(task).categories) { - val builder = CpoBuilder.newInsert(taskList.tasksPropertiesSyncUri()) + val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri()) .withTaskId(Category.TASK_ID, idxTask) .withValue(Category.MIMETYPE, Category.CONTENT_ITEM_TYPE) .withValue(Category.CATEGORY_NAME, category) @@ -447,7 +448,7 @@ class DmfsTask( private fun insertComment(batch: TasksBatchOperation, idxTask: Int?) { val comment = requireNotNull(task).comment ?: return - val builder = CpoBuilder.newInsert(taskList.tasksPropertiesSyncUri()) + val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri()) .withTaskId(Comment.TASK_ID, idxTask) .withValue(Comment.MIMETYPE, Comment.CONTENT_ITEM_TYPE) .withValue(Comment.COMMENT, comment) @@ -465,7 +466,7 @@ class DmfsTask( else /* RelType.PARENT, default value */ -> Relation.RELTYPE_PARENT } - val builder = CpoBuilder.newInsert(taskList.tasksPropertiesSyncUri()) + val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri()) .withTaskId(Relation.TASK_ID, idxTask) .withValue(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE) .withValue(Relation.RELATED_UID, relatedTo.value) @@ -482,7 +483,7 @@ class DmfsTask( return } - val builder = CpoBuilder.newInsert(taskList.tasksPropertiesSyncUri()) + val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri()) .withTaskId(Properties.TASK_ID, idxTask) .withValue(Properties.MIMETYPE, UnknownProperty.CONTENT_ITEM_TYPE) .withValue(UNKNOWN_PROPERTY_DATA, UnknownProperty.toJsonString(property)) @@ -492,7 +493,7 @@ class DmfsTask( } fun delete(): Int { - return taskList.provider.delete(taskSyncURI(), null, null) + return taskList.provider.client.delete(taskSyncURI(), null, null) } private fun buildTask(builder: CpoBuilder, update: Boolean) { @@ -617,7 +618,7 @@ class DmfsTask( private fun taskSyncURI(loadProperties: Boolean = false): Uri { val id = requireNotNull(id) - return ContentUris.withAppendedId(taskList.tasksSyncUri(loadProperties), id) + return ContentUris.withAppendedId(taskList.tasksUri(loadProperties), id) } companion object { diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt deleted file mode 100644 index b7cfd800..00000000 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskList.kt +++ /dev/null @@ -1,257 +0,0 @@ -/* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package at.bitfire.ical4android - -import android.accounts.Account -import android.content.ContentProviderClient -import android.content.ContentUris -import android.content.ContentValues -import android.net.Uri -import androidx.core.content.contentValuesOf -import at.bitfire.ical4android.DmfsTaskList.Companion.find -import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter -import at.bitfire.synctools.storage.BatchOperation -import at.bitfire.synctools.storage.LocalStorageException -import at.bitfire.synctools.storage.TasksBatchOperation -import at.bitfire.synctools.storage.toContentValues -import org.dmfs.tasks.contract.TaskContract -import org.dmfs.tasks.contract.TaskContract.Property.Relation -import org.dmfs.tasks.contract.TaskContract.TaskListColumns -import org.dmfs.tasks.contract.TaskContract.TaskLists -import org.dmfs.tasks.contract.TaskContract.Tasks -import java.io.FileNotFoundException -import java.util.LinkedList -import java.util.logging.Level -import java.util.logging.Logger - - -/** - * Represents a locally stored task list, containing [DmfsTask]s (tasks). - * Communicates with tasks.org-compatible content providers (currently tasks.org and OpenTasks) to store the tasks. - */ -class DmfsTaskList( - val account: Account, - val provider: ContentProviderClient, - val providerName: TaskProvider.ProviderName, - val id: Long -) { - - var syncId: String? = null - var name: String? = null - var accessLevel: Int? = null - var color: Int? = null - var isSynced = false - var isVisible = false - - - /** - * Sets the task list properties ([syncId], [name] etc.) from the passed argument, - * which is usually directly taken from the tasks provider. - * - * Called when an instance is created from a tasks provider data row, for example - * using [find]. - * - * @param values values from tasks provider - */ - private fun populate(values: ContentValues) { - syncId = values.getAsString(TaskLists._SYNC_ID) - name = values.getAsString(TaskLists.LIST_NAME) - accessLevel = values.getAsInteger(TaskListColumns.ACCESS_LEVEL) - color = values.getAsInteger(TaskLists.LIST_COLOR) - values.getAsInteger(TaskLists.SYNC_ENABLED)?.let { isSynced = it != 0 } - values.getAsInteger(TaskLists.VISIBLE)?.let { isVisible = it != 0 } - } - - fun update(info: ContentValues): Int { - logger.log(Level.FINE, "Updating ${providerName.authority} task list (#$id)", info) - return provider.update(taskListSyncUri(), info, null, null) - } - - /** - * Deletes this calendar from the local calendar provider. - * - * @return `true` if the calendar was deleted, `false` otherwise (like it was not there before the call) - */ - fun delete(): Boolean { - logger.log(Level.FINE, "Deleting ${providerName.authority} task list (#$id)") - return provider.delete(taskListSyncUri(), null, null) > 0 - } - - /** - * 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 touchRelations(): Int { - logger.fine("Touching relations to set parent_id") - val batch = TasksBatchOperation(provider) - provider.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 values = cursor.toContentValues() - val id = values.getAsLong(Relation.PROPERTY_ID) - val propertyContentUri = ContentUris.withAppendedId(tasksPropertiesSyncUri(), id) - batch += BatchOperation.CpoBuilder - .newUpdate(propertyContentUri) - .withValue(Relation.RELATED_ID, values.getAsLong(Relation.RELATED_ID)) - } - } - return 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? = null): List { - val where = "(${_where ?: "1"}) AND ${Tasks.LIST_ID}=?" - val whereArgs = (_whereArgs ?: arrayOf()) + id.toString() - - val tasks = LinkedList() - provider.query( - tasksSyncUri(), - null, - where, whereArgs, null - )?.use { cursor -> - while (cursor.moveToNext()) - tasks += DmfsTask(this, cursor.toContentValues()) - } - return tasks - } - - fun findById(id: Long) = queryTasks("${Tasks._ID}=?", arrayOf(id.toString())).firstOrNull() - ?: throw FileNotFoundException() - - fun readSyncState(): String? = try { - provider.query(taskListSyncUri(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor -> - if (cursor.moveToNext()) - return cursor.getString(0) - else - null - } - } catch (e: Exception) { - logger.log(Level.WARNING, "Couldn't read sync state", e) - null - } - - fun writeSyncState(state: String?) { - val values = contentValuesOf(COLUMN_SYNC_STATE to state) - provider.update(taskListSyncUri(), values, null, null) - } - - fun taskListSyncUri() = - ContentUris.withAppendedId(TaskLists.getContentUri(providerName.authority), id).asSyncAdapter(account) - - fun tasksSyncUri(loadProperties: Boolean = false): Uri { - val uri = Tasks.getContentUri(providerName.authority).asSyncAdapter(account) - return if (loadProperties) - uri.buildUpon() - .appendQueryParameter(TaskContract.LOAD_PROPERTIES, "1") - .build() - else - uri - } - - fun tasksPropertiesSyncUri() = TaskContract.Properties.getContentUri(providerName.authority).asSyncAdapter(account) - - companion object { - - private const val COLUMN_SYNC_STATE = TaskLists.SYNC_VERSION - - private val logger - get() = Logger.getLogger(DmfsTaskList::class.java.name) - - fun create(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, info: ContentValues): Uri { - info.put(TaskContract.ACCOUNT_NAME, account.name) - info.put(TaskContract.ACCOUNT_TYPE, account.type) - - val url = TaskLists.getContentUri(providerName.authority).asSyncAdapter(account) - logger.log(Level.FINE, "Creating ${providerName.authority} task list", info) - return provider.insert(url, info) - ?: throw LocalStorageException("Couldn't create task list (empty result from provider)") - } - - fun findByID( - account: Account, - provider: ContentProviderClient, - providerName: TaskProvider.ProviderName, - id: Long - ): DmfsTaskList { - provider.query( - ContentUris.withAppendedId(TaskLists.getContentUri(providerName.authority), id).asSyncAdapter(account), - null, - null, - null, - null - )?.use { cursor -> - if (cursor.moveToNext()) { - val taskList = DmfsTaskList(account, provider, providerName, id) - taskList.populate(cursor.toContentValues()) - return taskList - } - } - throw FileNotFoundException() - } - - fun find( - account: Account, - provider: ContentProviderClient, - providerName: TaskProvider.ProviderName, - where: String?, - whereArgs: Array? - ): List { - val taskLists = LinkedList() - provider.query( - TaskLists.getContentUri(providerName.authority).asSyncAdapter(account), - null, - where, - whereArgs, - null - )?.use { cursor -> - while (cursor.moveToNext()) { - val values = cursor.toContentValues() - val taskList = DmfsTaskList( - account = account, - provider = provider, - providerName = providerName, - id = values.getAsLong(TaskLists._ID) - ) - taskList.populate(values) - taskLists += taskList - } - } - return taskLists - } - - } - -} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt new file mode 100644 index 00000000..5a07889a --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt @@ -0,0 +1,195 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.storage.tasks + +import android.content.ContentUris +import android.content.ContentValues +import android.net.Uri +import at.bitfire.ical4android.DmfsTask +import at.bitfire.ical4android.TaskProvider +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.LocalStorageException +import at.bitfire.synctools.storage.toContentValues +import org.dmfs.tasks.contract.TaskContract +import java.util.LinkedList +import java.util.logging.Logger + +/** + * Represents a locally stored task list, containing [at.bitfire.ical4android.DmfsTask]s (tasks). + * Communicates with tasks.org-compatible content providers (currently tasks.org and OpenTasks) to store the tasks. + */ +class DmfsTaskList( + val provider: DmfsTaskListProvider, + val values: ContentValues, + val providerName: TaskProvider.ProviderName, +) { + + private val logger + get() = Logger.getLogger(DmfsTaskList::class.java.name) + + /** see [TaskContract.TaskLists._ID] **/ + val id: Long = values.getAsLong(TaskContract.TaskLists._ID) + ?: throw IllegalArgumentException("${TaskContract.TaskLists._ID} must be set") + + /** see [TaskContract.TaskListColumns.ACCESS_LEVEL] **/ + val accessLevel: Int + get() = values.getAsInteger(TaskContract.TaskListColumns.ACCESS_LEVEL) ?: 0 + + /** see [TaskContract.TaskLists.LIST_NAME] **/ + val name: String? + get() = values.getAsString(TaskContract.TaskLists.LIST_NAME) + + /** see [TaskContract.TaskLists._SYNC_ID] **/ + val syncId: String? + get() = values.getAsString(TaskContract.TaskLists._SYNC_ID) + + + // CRUD DmfsTask + + /** + * Queries tasks from this task list. Adds a WHERE clause that restricts the + * query to [TaskContract.TaskColumns.LIST_ID] = [id]. + * + * @param where selection + * @param whereArgs arguments for selection + * + * @return events from this task list which match the selection + */ + fun findTasks(where: String? = null, whereArgs: Array? = null): List { + val tasks = LinkedList() + try { + val (protectedWhere, protectedWhereArgs) = whereWithTaskListId(where, whereArgs) + client.query(tasksUri(), null, protectedWhere, protectedWhereArgs, null)?.use { cursor -> + while (cursor.moveToNext()) + tasks += DmfsTask(this, cursor.toContentValues()) + } + } catch (e: Exception) { + throw LocalStorageException("Couldn't query ${providerName.authority} tasks", e) + } + return tasks + } + + fun getTask(id: Long) = findTasks("${TaskContract.Tasks._ID}=?", arrayOf(id.toString())).firstOrNull() + ?: throw LocalStorageException("Couldn't query ${providerName.authority} tasks") + + /** + * Updates tasks in this task list. + * + * @param values values to update + * @param where selection + * @param whereArgs arguments for selection + * + * @return number of updated rows + * @throws LocalStorageException when the content provider returns an error + */ + fun updateTasks(values: ContentValues, where: String?, whereArgs: Array?): Int = + try { + client.update(tasksUri(), values, where, whereArgs) + } catch (e: Exception) { + throw LocalStorageException("Couldn't update ${providerName.authority} tasks", e) + } + + + // shortcuts to upper level + + /** Calls [DmfsTaskListProvider.delete] for this task list **/ + fun delete(): Boolean = provider.delete(id) + + /** + * Calls [DmfsTaskListProvider.updateTaskList] for this task list. + * + * **Attention**: Does not update this object with the new values! + */ + fun update(values: ContentValues): Int = provider.updateTaskList(id,values) + + /** Calls [DmfsTaskListProvider.readTaskListSyncState] for this task list. */ + fun readSyncState(): String? = provider.readTaskListSyncState(id) + + /** Calls [DmfsTaskListProvider.writeTaskListSyncState] for this task list. */ + fun writeSyncState(state: String?) = provider.writeTaskListSyncState(id, state) + + + // helpers + + val account + get() = provider.account + + val client + get() = provider.client + + fun tasksUri(loadProperties: Boolean = false): Uri { + val uri = TaskContract.Tasks.getContentUri(providerName.authority).asSyncAdapter(account) + return if (loadProperties) + uri.buildUpon() + .appendQueryParameter(TaskContract.LOAD_PROPERTIES, "1") + .build() + else + uri + } + + fun tasksPropertiesUri() = + TaskContract.Properties.getContentUri(providerName.authority).asSyncAdapter(account) + + /** + * Restricts a given selection/where clause to this task list ID. + * + * @param where selection + * @param whereArgs arguments for selection + * @return restricted selection and arguments + */ + private fun whereWithTaskListId(where: String?, whereArgs: Array?): Pair> { + val protectedWhere = "(${where ?: "1"}) AND ${TaskContract.Tasks.LIST_ID}=?" + val protectedWhereArgs = (whereArgs ?: arrayOf()) + id.toString() + return Pair(protectedWhere, protectedWhereArgs) + } + + /** + * When tasks are added or updated, they may refer to related tasks by UID ([TaskContract.Property.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 + * [TaskContract.PropertyColumns.TASK_ID]) in the database. + * + * As soon as such a related task is added, OpenTasks updates the [TaskContract.Property.Relation.RELATED_ID], + * but it does *not* update [TaskContract.TaskColumns.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 [TaskContract.Property.Relation] rows + * - with [TaskContract.Property.Relation.RELATED_ID] (→ related task is already synchronized) + * - of tasks without [TaskContract.TaskColumns.PARENT_ID] (→ only touch relevant rows) + * + * so that missing [TaskContract.TaskColumns.PARENT_ID] fields are updated. + * + * @return number of touched [TaskContract.Property.Relation] rows + */ + fun touchRelations(): Int { + logger.fine("Touching relations to set parent_id") + val batch = TasksBatchOperation(client) + client.query( + tasksUri(true), null, + "${TaskContract.Tasks.LIST_ID}=? AND ${TaskContract.Tasks.PARENT_ID} IS NULL AND ${TaskContract.Property.Relation.MIMETYPE}=? AND ${TaskContract.Property.Relation.RELATED_ID} IS NOT NULL", + arrayOf(id.toString(), TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + null, null + )?.use { cursor -> + while (cursor.moveToNext()) { + val values = cursor.toContentValues() + val id = values.getAsLong(TaskContract.Property.Relation.PROPERTY_ID) + val propertyContentUri = ContentUris.withAppendedId(tasksPropertiesUri(), id) + batch += BatchOperation.CpoBuilder + .newUpdate(propertyContentUri) + .withValue( + TaskContract.Property.Relation.RELATED_ID, + values.getAsLong(TaskContract.Property.Relation.RELATED_ID) + ) + } + } + return batch.commit() + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt new file mode 100644 index 00000000..532895ee --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt @@ -0,0 +1,189 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.storage.tasks + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.ContentValues +import android.os.RemoteException +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.TaskProvider +import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter +import at.bitfire.synctools.storage.LocalStorageException +import at.bitfire.synctools.storage.toContentValues +import org.dmfs.tasks.contract.TaskContract +import java.util.LinkedList +import java.util.logging.Level +import java.util.logging.Logger + +/** + * Manages locally stored tasklists (represented by [DmfsTaskList]) in + * DmfsTask tasks provider. + * + * @param account Account that all operations are bound to + * @param client content provider client + */ +class DmfsTaskListProvider( + val account: Account, + internal val client: ContentProviderClient, + val providerName: TaskProvider.ProviderName, +) { + + private val logger + get() = Logger.getLogger(DmfsTaskList::class.java.name) + + + // DmfsTaskList CRUD + + fun createTaskList(values: ContentValues): Long { + logger.log(Level.FINE, "Creating ${providerName.authority} local task list", values) + + values.put(TaskContract.ACCOUNT_NAME, account.name) + values.put(TaskContract.ACCOUNT_TYPE, account.type) + + val uri = try { + client.insert(taskListsUri, values) + } catch (e: Exception) { + throw LocalStorageException("Couldn't create task list", e) + } + if (uri == null) + throw LocalStorageException("Couldn't create task list (empty result from provider)") + return ContentUris.parseId(uri) + } + + /** + * Creates a new task list and directly returns it. + * + * @param values values to create the task list from (account name and type are inserted) + * + * @return the created task list + * @throws LocalStorageException when the content provider returns nothing or an error + */ + fun createAndGetTaskList(values: ContentValues): DmfsTaskList { + val id = createTaskList(values) + return getTaskList(id) ?: throw LocalStorageException("Couldn't query ${providerName.authority} task list that was just created") + } + + /** + * Queries existing task lists. + * + * @param where selection + * @param whereArgs arguments for selection + * @param sortOrder sort order + * + * @return list of task lists + * @throws LocalStorageException when the content provider returns an error + */ + fun findTaskLists(where: String? = null, whereArgs: Array? = null, sortOrder: String? = null): List { + val result = LinkedList() + try { + client.query(taskListsUri, null, where, whereArgs, sortOrder)?.use { cursor -> + while (cursor.moveToNext()) + result += DmfsTaskList(this, cursor.toContentValues(), providerName) + } + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't query ${providerName.authority} task lists", e) + } + return result + } + + /** + * Queries existing task lists and returns the first task list that matches the search criteria. + * + * @param where selection + * @param whereArgs arguments for selection + * @param sortOrder sort order + * + * @return first task list that matches the search criteria (or `null`) + * @throws LocalStorageException when the content provider returns an error + */ + fun findFirstTaskList(where: String?, whereArgs: Array?, sortOrder: String? = null): DmfsTaskList? { + try { + client.query(taskListsUri, null, where, whereArgs, sortOrder)?.use { cursor -> + if (cursor.moveToNext()) + return DmfsTaskList(this, cursor.toContentValues(), providerName) + } + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't query ${providerName.authority} task lists", e) + } + return null + } + + /** + * Gets an existing task list by its ID. + * + * @param id task list ID + * + * @return task list (or `null` if not found) + * @throws LocalStorageException when the content provider returns an error + */ + fun getTaskList(id: Long): DmfsTaskList? { + try { + client.query(taskListUri(id), null, null, null, null)?.use { cursor -> + if (cursor.moveToNext()) + return DmfsTaskList(this, cursor.toContentValues(), providerName) + } + } catch (e: Exception) { + throw LocalStorageException("Couldn't query ${providerName.authority} task list", e) + } + return null + } + + fun updateTaskList(id: Long, info: ContentValues): Int { + logger.log(Level.FINE, "Updating ${providerName.authority} task list (#$id)", info) + return client.update(taskListUri(id), info, null, null) + } + + /** + * Deletes this task list from the local task list provider. + * + * @return `true` if the task list was deleted, `false` otherwise (like it was not there before the call) + */ + fun delete(id: Long): Boolean { + logger.log(Level.FINE, "Deleting ${providerName.authority} task list (#$id)") + return client.delete(taskListUri(id), null, null) > 0 + } + + + // other methods: sync state + + fun readTaskListSyncState(id: Long): String? = try { + client.query(taskListUri(id), arrayOf(COLUMN_TASKLIST_SYNC_STATE), null, null, null)?.use { cursor -> + if (cursor.moveToNext()) + return cursor.getString(0) + else + null + } + } catch (e: Exception) { + throw LocalStorageException("Couldn't query ${providerName.authority} task list sync state", e) + } + + fun writeTaskListSyncState(id: Long, state: String?) { + val values = contentValuesOf(COLUMN_TASKLIST_SYNC_STATE to state) + client.update(taskListUri(id), values, null, null) + } + + + // helpers + + val taskListsUri + get() = TaskContract.TaskLists.getContentUri(providerName.authority).asSyncAdapter(account) + + fun taskListUri(id: Long) = + ContentUris.withAppendedId(taskListsUri, id) + + companion object { + + /** + * Column to store per task list sync state as a [String]. + */ + private const val COLUMN_TASKLIST_SYNC_STATE = TaskContract.TaskLists.SYNC_VERSION + + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/TasksBatchOperation.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/TasksBatchOperation.kt similarity index 79% rename from lib/src/main/kotlin/at/bitfire/synctools/storage/TasksBatchOperation.kt rename to lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/TasksBatchOperation.kt index 0f12d877..ee8cacd8 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/TasksBatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/TasksBatchOperation.kt @@ -4,12 +4,13 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package at.bitfire.synctools.storage +package at.bitfire.synctools.storage.tasks import android.content.ContentProviderClient +import at.bitfire.synctools.storage.BatchOperation /** - * [BatchOperation] for the tasks.org / OpenTasks provider + * [at.bitfire.synctools.storage.BatchOperation] for the tasks.org / OpenTasks provider */ class TasksBatchOperation( providerClient: ContentProviderClient From d63a07c50843bfb9257500b221fe9998e2aae881 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Mon, 19 Jan 2026 14:41:43 +0100 Subject: [PATCH 2/8] Adapt tests --- .../at/bitfire/ical4android/DmfsTaskTest.kt | 18 +++++++-------- .../bitfire/ical4android/impl/TestTaskList.kt | 5 +++-- .../tasks/ContactsBatchOperationTest.kt | 1 + .../storage/tasks/DmfsTaskListTest.kt | 22 +++++++++---------- .../at/bitfire/ical4android/DmfsTask.kt | 2 +- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index 816b17b9..5b1f2588 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -514,7 +514,7 @@ class DmfsTaskTest( categories.addAll(arrayOf("Cat_1", "Cat 2")) }.let { result -> val id = result.getAsLong(Tasks._ID) - val uri = taskList!!.tasksPropertiesSyncUri() + val uri = taskList!!.tasksPropertiesUri() provider.client.query(uri, arrayOf(Category.CATEGORY_NAME), "${Properties.MIMETYPE}=? AND ${PropertyColumns.TASK_ID}=?", arrayOf(Category.CONTENT_ITEM_TYPE, id.toString()), null)!!.use { cursor -> while (cursor.moveToNext()) @@ -535,7 +535,7 @@ class DmfsTaskTest( comment = "Comment value" }.let { result -> val id = result.getAsLong(Tasks._ID) - val uri = taskList!!.tasksPropertiesSyncUri() + val uri = taskList!!.tasksPropertiesUri() provider.client.query(uri, arrayOf(Property.Comment.COMMENT), "${Properties.MIMETYPE}=? AND ${PropertyColumns.TASK_ID}=?", arrayOf(Property.Comment.CONTENT_ITEM_TYPE, id.toString()), null)!!.use { cursor -> if (cursor.moveToNext()) @@ -552,7 +552,7 @@ class DmfsTaskTest( comment = null }.let { result -> val id = result.getAsLong(Tasks._ID) - val uri = taskList!!.tasksPropertiesSyncUri() + val uri = taskList!!.tasksPropertiesUri() provider.client.query(uri, arrayOf(Property.Comment.COMMENT), "${Properties.MIMETYPE}=? AND ${PropertyColumns.TASK_ID}=?", arrayOf(Property.Comment.CONTENT_ITEM_TYPE, id.toString()), null)!!.use { cursor -> hasComment = cursor.count > 0 @@ -562,7 +562,7 @@ class DmfsTaskTest( } private fun firstProperty(taskId: Long, mimeType: String): ContentValues? { - val uri = taskList!!.tasksPropertiesSyncUri() + val uri = taskList!!.tasksPropertiesUri() provider.client.query(uri, null, "${Properties.MIMETYPE}=? AND ${PropertyColumns.TASK_ID}=?", arrayOf(mimeType, taskId.toString()), null)!!.use { cursor -> if (cursor.moveToNext()) { @@ -692,7 +692,7 @@ class DmfsTaskTest( assertNotNull("Couldn't add task", uri) // read and parse event from calendar provider - val testTask = taskList!!.findById(ContentUris.parseId(uri)) + val testTask = taskList!!.getTask(ContentUris.parseId(uri)) try { assertNotNull("Inserted task is not here", testTask) val task2 = testTask.task @@ -735,7 +735,7 @@ class DmfsTaskTest( task.alarms += VAlarm(java.time.Duration.ofMinutes(i.toLong())) val uri = DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() - val task2 = taskList!!.findById(ContentUris.parseId(uri)) + val task2 = taskList!!.getTask(ContentUris.parseId(uri)) assertEquals(1050, task2.task?.alarms?.size) } @@ -752,7 +752,7 @@ class DmfsTaskTest( val uri = DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() assertNotNull(uri) - val testTask = taskList!!.findById(ContentUris.parseId(uri)) + val testTask = taskList!!.getTask(ContentUris.parseId(uri)) try { // update test event in calendar val task2 = testTask.task!! @@ -762,7 +762,7 @@ class DmfsTaskTest( testTask.update(task2) // read again and verify result - val updatedTask = taskList!!.findById(ContentUris.parseId(uri)).task!! + val updatedTask = taskList!!.getTask(ContentUris.parseId(uri)).task!! assertEquals(task2.summary, updatedTask.summary) assertEquals(task2.location, updatedTask.location) assertEquals(task2.dtStart, updatedTask.dtStart) @@ -785,7 +785,7 @@ class DmfsTaskTest( val uri = DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() assertNotNull(uri) - val testTask = taskList!!.findById(ContentUris.parseId(uri)) + val testTask = taskList!!.getTask(ContentUris.parseId(uri)) try { // read again and verify result val task2 = testTask.task!! diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt index 15ec5403..66416b18 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt @@ -11,6 +11,7 @@ import android.content.ContentUris import android.content.ContentValues import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.ical4android.TaskProvider +import at.bitfire.synctools.storage.tasks.DmfsTaskListProvider import org.dmfs.tasks.contract.TaskContract object TestTaskList { @@ -24,9 +25,9 @@ object TestTaskList { values.put(TaskContract.TaskListColumns.LIST_COLOR, 0xffff0000) values.put(TaskContract.TaskListColumns.SYNC_ENABLED, 1) values.put(TaskContract.TaskListColumns.VISIBLE, 1) - val uri = DmfsTaskList.create(account, provider.client, provider.name, values) + val dmfsTaskListProvider = DmfsTaskListProvider(account, provider.client, provider.name) - return DmfsTaskList(account, provider.client, provider.name, ContentUris.parseId(uri)) + return DmfsTaskList(dmfsTaskListProvider, values, provider.name) } } diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/ContactsBatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/ContactsBatchOperationTest.kt index e5c5faa4..6ef2e3af 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/ContactsBatchOperationTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/ContactsBatchOperationTest.kt @@ -16,6 +16,7 @@ import at.bitfire.ical4android.util.MiscUtils.closeCompat import at.bitfire.synctools.storage.BatchOperation import at.bitfire.synctools.storage.ContactsBatchOperation import at.bitfire.synctools.storage.LocalStorageException +import at.bitfire.synctools.test.BuildConfig import org.junit.After import org.junit.Before import org.junit.Rule diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt index 4eb46f61..fe40da99 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt @@ -34,12 +34,12 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): info.put(TaskContract.TaskLists.VISIBLE, 1) val dmfsTaskListProvider = DmfsTaskListProvider(testAccount, provider.client, providerName) - val uri = dmfsTaskListProvider.createTaskList(testAccount, provider.client, providerName, info) - Assert.assertNotNull(uri) + val id = dmfsTaskListProvider.createTaskList(info) + Assert.assertNotNull(id) - dmfsTaskListProvider.createTaskList() + dmfsTaskListProvider.createTaskList(info) - return DmfsTaskList.findByID(testAccount, provider.client, providerName, ContentUris.parseId(uri)) + return dmfsTaskListProvider.getTaskList(id)!! } @Test @@ -50,28 +50,28 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): // sync URIs Assert.assertEquals( "true", - taskList.taskListSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) + taskList.tasksUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) ) Assert.assertEquals( testAccount.type, - taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) + taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) ) Assert.assertEquals( testAccount.name, - taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME) + taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_NAME) ) Assert.assertEquals( "true", - taskList.tasksSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) + taskList.tasksUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) ) Assert.assertEquals( testAccount.type, - taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) + taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) ) Assert.assertEquals( testAccount.name, - taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME) + taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_NAME) ) } finally { // delete task list @@ -111,7 +111,7 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): val parentId = ContentUris.parseId(parentContentUri) // OpenTasks should provide the correct relation - taskList.provider.client.query(taskList.tasksPropertiesSyncUri(), null, + taskList.provider.client.query(taskList.tasksPropertiesUri(), null, "${TaskContract.Properties.TASK_ID}=?", arrayOf(childId.toString()), null, null)!!.use { cursor -> Assert.assertEquals(1, cursor.count) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt index c07fd5d7..eca00e40 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt @@ -12,8 +12,8 @@ import android.net.Uri import android.os.RemoteException import at.bitfire.synctools.storage.BatchOperation.CpoBuilder import at.bitfire.synctools.storage.LocalStorageException -import at.bitfire.synctools.storage.tasks.TasksBatchOperation import at.bitfire.synctools.storage.tasks.DmfsTaskList +import at.bitfire.synctools.storage.tasks.TasksBatchOperation import at.bitfire.synctools.storage.toContentValues import at.bitfire.synctools.util.AndroidTimeUtils import net.fortuna.ical4j.model.Date From eb9f33d920666036ba00eab9a972a5cc4bfdbff1 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 20 Jan 2026 13:38:04 +0100 Subject: [PATCH 3/8] Use TaskContract.LOCAL_ACCOUNT_TYPE instead of from CalendarContract --- .../kotlin/at/bitfire/ical4android/DmfsTaskTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index 5b1f2588..1012fba8 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -11,7 +11,6 @@ import android.content.ContentUris import android.content.ContentValues import android.database.DatabaseUtils import android.net.Uri -import android.provider.CalendarContract import androidx.core.content.contentValuesOf import at.bitfire.ical4android.impl.TestTaskList import at.bitfire.synctools.storage.LocalStorageException @@ -39,6 +38,7 @@ import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RelatedTo import net.fortuna.ical4j.model.property.Status import net.fortuna.ical4j.model.property.XProperty +import org.dmfs.tasks.contract.TaskContract import org.dmfs.tasks.contract.TaskContract.Properties import org.dmfs.tasks.contract.TaskContract.Property import org.dmfs.tasks.contract.TaskContract.Property.Category @@ -64,7 +64,7 @@ class DmfsTaskTest( private val tzChicago = tzRegistry.getTimeZone("America/Chicago")!! private val tzDefault = tzRegistry.getTimeZone(ZoneId.systemDefault().id)!! - private val testAccount = Account(javaClass.name, CalendarContract.ACCOUNT_TYPE_LOCAL) + private val testAccount = Account(javaClass.name, TaskContract.LOCAL_ACCOUNT_TYPE) private lateinit var taskListUri: Uri private var taskList: DmfsTaskList? = null From 477fba007b53a4e79af2206ca6d9d5417acdfe7c Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 20 Jan 2026 13:41:09 +0100 Subject: [PATCH 4/8] Optimize imports --- .../kotlin/at/bitfire/ical4android/impl/TestTaskList.kt | 3 +-- .../at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt index 66416b18..c219a9f2 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt @@ -7,10 +7,9 @@ package at.bitfire.ical4android.impl import android.accounts.Account -import android.content.ContentUris import android.content.ContentValues -import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.ical4android.TaskProvider +import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.synctools.storage.tasks.DmfsTaskListProvider import org.dmfs.tasks.contract.TaskContract diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt index fe40da99..106aaebf 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt @@ -10,7 +10,6 @@ import android.accounts.Account import android.content.ContentUris import android.content.ContentValues import android.database.DatabaseUtils -import android.provider.CalendarContract import at.bitfire.ical4android.DmfsStyleProvidersTaskTest import at.bitfire.ical4android.DmfsTask import at.bitfire.ical4android.Task @@ -23,7 +22,7 @@ import org.junit.Test class DmfsTaskListTest(providerName: TaskProvider.ProviderName): DmfsStyleProvidersTaskTest(providerName) { - private val testAccount = Account(javaClass.name, CalendarContract.ACCOUNT_TYPE_LOCAL) + private val testAccount = Account(javaClass.name, TaskContract.LOCAL_ACCOUNT_TYPE) private fun createTaskList(): DmfsTaskList { val info = ContentValues() From 0b91b21025168ba19c4c8058308e2c20e1323e2f Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 20 Jan 2026 15:05:45 +0100 Subject: [PATCH 5/8] Add deleteTasks method --- .../synctools/storage/tasks/DmfsTaskList.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt index 5a07889a..8f2dca52 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt @@ -94,6 +94,22 @@ class DmfsTaskList( throw LocalStorageException("Couldn't update ${providerName.authority} tasks", e) } + /** + * Deletes tasks in this task list. + * + * @param where selection + * @param whereArgs arguments for selection + * + * @return number of deleted rows + * @throws LocalStorageException when the content provider returns an error + */ + fun deleteTasks(where: String?, whereArgs: Array?): Int = + try { + client.delete(tasksUri(), where, whereArgs) + } catch (e: Exception) { + throw LocalStorageException("Couldn't delete ${providerName.authority} tasks", e) + } + // shortcuts to upper level From 0a311e945e82e435a098317cd26a91f8dc39c2ba Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 21 Jan 2026 10:51:51 +0100 Subject: [PATCH 6/8] Remove trailing commas --- .../kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt | 2 +- .../at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt index 8f2dca52..f99eef94 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt @@ -26,7 +26,7 @@ import java.util.logging.Logger class DmfsTaskList( val provider: DmfsTaskListProvider, val values: ContentValues, - val providerName: TaskProvider.ProviderName, + val providerName: TaskProvider.ProviderName ) { private val logger diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt index 532895ee..7cd4c139 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt @@ -31,7 +31,7 @@ import java.util.logging.Logger class DmfsTaskListProvider( val account: Account, internal val client: ContentProviderClient, - val providerName: TaskProvider.ProviderName, + val providerName: TaskProvider.ProviderName ) { private val logger From 95161c2090cc0265ea61d5e0242595cccc2e0ced Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 21 Jan 2026 11:18:09 +0100 Subject: [PATCH 7/8] Use RemoteException everywhere --- .../synctools/storage/tasks/DmfsTaskList.kt | 47 ++++++++++--------- .../storage/tasks/DmfsTaskListProvider.kt | 43 +++++++++++------ 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt index f99eef94..cffbd563 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt @@ -9,6 +9,7 @@ package at.bitfire.synctools.storage.tasks import android.content.ContentUris import android.content.ContentValues import android.net.Uri +import android.os.RemoteException import at.bitfire.ical4android.DmfsTask import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter @@ -68,7 +69,7 @@ class DmfsTaskList( while (cursor.moveToNext()) tasks += DmfsTask(this, cursor.toContentValues()) } - } catch (e: Exception) { + } catch (e: RemoteException) { throw LocalStorageException("Couldn't query ${providerName.authority} tasks", e) } return tasks @@ -90,7 +91,7 @@ class DmfsTaskList( fun updateTasks(values: ContentValues, where: String?, whereArgs: Array?): Int = try { client.update(tasksUri(), values, where, whereArgs) - } catch (e: Exception) { + } catch (e: RemoteException) { throw LocalStorageException("Couldn't update ${providerName.authority} tasks", e) } @@ -106,7 +107,7 @@ class DmfsTaskList( fun deleteTasks(where: String?, whereArgs: Array?): Int = try { client.delete(tasksUri(), where, whereArgs) - } catch (e: Exception) { + } catch (e: RemoteException) { throw LocalStorageException("Couldn't delete ${providerName.authority} tasks", e) } @@ -186,26 +187,30 @@ class DmfsTaskList( */ fun touchRelations(): Int { logger.fine("Touching relations to set parent_id") - val batch = TasksBatchOperation(client) - client.query( - tasksUri(true), null, - "${TaskContract.Tasks.LIST_ID}=? AND ${TaskContract.Tasks.PARENT_ID} IS NULL AND ${TaskContract.Property.Relation.MIMETYPE}=? AND ${TaskContract.Property.Relation.RELATED_ID} IS NOT NULL", - arrayOf(id.toString(), TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - null, null - )?.use { cursor -> - while (cursor.moveToNext()) { - val values = cursor.toContentValues() - val id = values.getAsLong(TaskContract.Property.Relation.PROPERTY_ID) - val propertyContentUri = ContentUris.withAppendedId(tasksPropertiesUri(), id) - batch += BatchOperation.CpoBuilder - .newUpdate(propertyContentUri) - .withValue( - TaskContract.Property.Relation.RELATED_ID, - values.getAsLong(TaskContract.Property.Relation.RELATED_ID) - ) + try { + val batch = TasksBatchOperation(client) + client.query( + tasksUri(true), null, + "${TaskContract.Tasks.LIST_ID}=? AND ${TaskContract.Tasks.PARENT_ID} IS NULL AND ${TaskContract.Property.Relation.MIMETYPE}=? AND ${TaskContract.Property.Relation.RELATED_ID} IS NOT NULL", + arrayOf(id.toString(), TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + null, null + )?.use { cursor -> + while (cursor.moveToNext()) { + val values = cursor.toContentValues() + val id = values.getAsLong(TaskContract.Property.Relation.PROPERTY_ID) + val propertyContentUri = ContentUris.withAppendedId(tasksPropertiesUri(), id) + batch += BatchOperation.CpoBuilder + .newUpdate(propertyContentUri) + .withValue( + TaskContract.Property.Relation.RELATED_ID, + values.getAsLong(TaskContract.Property.Relation.RELATED_ID) + ) + } } + return batch.commit() + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't touch ${providerName.authority} task relations", e) } - return batch.commit() } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt index 7cd4c139..244f9dee 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt @@ -48,7 +48,7 @@ class DmfsTaskListProvider( val uri = try { client.insert(taskListsUri, values) - } catch (e: Exception) { + } catch (e: RemoteException) { throw LocalStorageException("Couldn't create task list", e) } if (uri == null) @@ -128,7 +128,7 @@ class DmfsTaskListProvider( if (cursor.moveToNext()) return DmfsTaskList(this, cursor.toContentValues(), providerName) } - } catch (e: Exception) { + } catch (e: RemoteException) { throw LocalStorageException("Couldn't query ${providerName.authority} task list", e) } return null @@ -136,7 +136,11 @@ class DmfsTaskListProvider( fun updateTaskList(id: Long, info: ContentValues): Int { logger.log(Level.FINE, "Updating ${providerName.authority} task list (#$id)", info) - return client.update(taskListUri(id), info, null, null) + try { + return client.update(taskListUri(id), info, null, null) + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't update ${providerName.authority} task list", e) + } } /** @@ -146,26 +150,35 @@ class DmfsTaskListProvider( */ fun delete(id: Long): Boolean { logger.log(Level.FINE, "Deleting ${providerName.authority} task list (#$id)") - return client.delete(taskListUri(id), null, null) > 0 + try { + return client.delete(taskListUri(id), null, null) > 0 + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't delete ${providerName.authority} task list", e) + } } // other methods: sync state - fun readTaskListSyncState(id: Long): String? = try { - client.query(taskListUri(id), arrayOf(COLUMN_TASKLIST_SYNC_STATE), null, null, null)?.use { cursor -> - if (cursor.moveToNext()) - return cursor.getString(0) - else - null + fun readTaskListSyncState(id: Long): String? = + try { + client.query(taskListUri(id), arrayOf(COLUMN_TASKLIST_SYNC_STATE), null, null, null)?.use { cursor -> + if (cursor.moveToNext()) + return cursor.getString(0) + else + null + } + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't query ${providerName.authority} task list sync state", e) } - } catch (e: Exception) { - throw LocalStorageException("Couldn't query ${providerName.authority} task list sync state", e) - } fun writeTaskListSyncState(id: Long, state: String?) { - val values = contentValuesOf(COLUMN_TASKLIST_SYNC_STATE to state) - client.update(taskListUri(id), values, null, null) + try { + val values = contentValuesOf(COLUMN_TASKLIST_SYNC_STATE to state) + client.update(taskListUri(id), values, null, null) + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't query ${providerName.authority} task list", e) + } } From b958d21e4650d93e862028a45c7ace7ce0d93679 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Wed, 21 Jan 2026 11:35:22 +0100 Subject: [PATCH 8/8] Remove useless test --- .../storage/tasks/DmfsTaskListTest.kt | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt index 106aaebf..d3ed1050 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt @@ -41,43 +41,6 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName): return dmfsTaskListProvider.getTaskList(id)!! } - @Test - fun testManageTaskLists() { - val taskList = createTaskList() - - try { - // sync URIs - Assert.assertEquals( - "true", - taskList.tasksUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) - ) - Assert.assertEquals( - testAccount.type, - taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) - ) - Assert.assertEquals( - testAccount.name, - taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_NAME) - ) - - Assert.assertEquals( - "true", - taskList.tasksUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER) - ) - Assert.assertEquals( - testAccount.type, - taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_TYPE) - ) - Assert.assertEquals( - testAccount.name, - taskList.tasksUri().getQueryParameter(TaskContract.ACCOUNT_NAME) - ) - } finally { - // delete task list - Assert.assertTrue(taskList.delete()) - } - } - @Test fun testTouchRelations() { val taskList = createTaskList()