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..1012fba8 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -11,10 +11,10 @@ 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 +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 @@ -38,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 @@ -63,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 @@ -513,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()) @@ -534,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()) @@ -551,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 @@ -561,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()) { @@ -691,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 @@ -734,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) } @@ -751,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!! @@ -761,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) @@ -784,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 64faeb86..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,10 @@ 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.ical4android.TaskProvider +import at.bitfire.synctools.storage.tasks.DmfsTaskList +import at.bitfire.synctools.storage.tasks.DmfsTaskListProvider import org.dmfs.tasks.contract.TaskContract object TestTaskList { @@ -24,9 +24,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/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..6ef2e3af 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,6 +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.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 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..d3ed1050 --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt @@ -0,0 +1,117 @@ +/* + * 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 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, TaskContract.LOCAL_ACCOUNT_TYPE) + + 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 id = dmfsTaskListProvider.createTaskList(info) + Assert.assertNotNull(id) + + dmfsTaskListProvider.createTaskList(info) + + return dmfsTaskListProvider.getTaskList(id)!! + } + + @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.tasksPropertiesUri(), 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..eca00e40 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.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 @@ -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..cffbd563 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt @@ -0,0 +1,216 @@ +/* + * 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 android.os.RemoteException +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: RemoteException) { + 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: RemoteException) { + 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: RemoteException) { + throw LocalStorageException("Couldn't delete ${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") + 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) + } + } + +} \ 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..244f9dee --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListProvider.kt @@ -0,0 +1,202 @@ +/* + * 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: RemoteException) { + 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: RemoteException) { + 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) + try { + return client.update(taskListUri(id), info, null, null) + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't update ${providerName.authority} task list", e) + } + } + + /** + * 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)") + 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 + } + } catch (e: RemoteException) { + throw LocalStorageException("Couldn't query ${providerName.authority} task list sync state", e) + } + + fun writeTaskListSyncState(id: Long, state: String?) { + 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) + } + } + + + // 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