Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ class DmfsTaskTest(
assertNotNull("Couldn't add task", uri)

// read and parse event from calendar provider
val testTask = taskList!!.getTask(ContentUris.parseId(uri))
val testTask = taskList!!.getLegacyTask(ContentUris.parseId(uri))
try {
assertNotNull("Inserted task is not here", testTask)
val task2 = testTask.task
val task2 = testTask?.task
assertNotNull("Inserted task is empty", task2)

// compare with original event
Expand All @@ -124,7 +124,7 @@ class DmfsTaskTest(
assertEquals(task.relatedTo, task2.relatedTo)
assertEquals(task.unknownProperties, task2.unknownProperties)
} finally {
testTask.delete()
testTask?.delete()
}
}

Expand All @@ -151,7 +151,7 @@ class DmfsTaskTest(

val uri = DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add()
val task2 = taskList!!.getTask(ContentUris.parseId(uri))
assertEquals(1050, task2.task?.alarms?.size)
assertEquals(1050, task2?.task?.alarms?.size)
}

@Test
Expand All @@ -170,20 +170,20 @@ class DmfsTaskTest(
val testTask = taskList!!.getTask(ContentUris.parseId(uri))
try {
// update test event in calendar
val task2 = testTask.task!!
val task2 = testTask?.task!!
task2.summary = "Updated event" // change value
task.location = null // remove value
task2.duration = Duration(java.time.Duration.ofMinutes(10)) // add value
testTask.update(task2)

// read again and verify result
val updatedTask = taskList!!.getTask(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)
assertEquals(task2.duration!!.value, updatedTask.duration!!.value)
} finally {
testTask.delete()
testTask?.delete()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -734,15 +734,15 @@ class DmfsTaskBuilderTest (
val testTask = taskList!!.getTask(ContentUris.parseId(uri))
try {
// read again and verify result
val task2 = testTask.task!!
val task2 = testTask?.task!!
assertEquals(task.summary, task2.summary)
assertEquals(task.description, task2.description)
assertEquals(task.location, task2.location)
assertEquals(task.dtStart!!.date, task2.dtStart!!.date)
assertEquals(task.due!!.date, task2.due!!.date)
Assert.assertTrue(task2.isAllDay())
} finally {
testTask.delete()
testTask?.delete()
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import java.util.logging.Logger
* The SEQUENCE field is stored in [Tasks.SYNC_VERSION], so don't use [Tasks.SYNC_VERSION]
* for anything else.
*/
@Deprecated("Use storage.tasks.DmfsTask instead")
class DmfsTask(
val taskList: DmfsTaskList
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

package at.bitfire.synctools.mapping.tasks

import at.bitfire.ical4android.DmfsTask.Companion.COLUMN_ETAG
import at.bitfire.ical4android.DmfsTask.Companion.COLUMN_FLAGS
import at.bitfire.ical4android.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA
import at.bitfire.ical4android.ICalendar
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.UnknownProperty
import at.bitfire.synctools.storage.BatchOperation.CpoBuilder
import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.COLUMN_ETAG
import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.COLUMN_FLAGS
import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA
import at.bitfire.synctools.storage.tasks.DmfsTaskList
import at.bitfire.synctools.storage.tasks.TasksBatchOperation
import at.bitfire.synctools.util.AndroidTimeUtils
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
package at.bitfire.synctools.mapping.tasks

import android.content.ContentValues
import at.bitfire.ical4android.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.UnknownProperty
import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA
import at.bitfire.synctools.storage.tasks.DmfsTaskList
import at.bitfire.synctools.util.AndroidTimeUtils
import net.fortuna.ical4j.model.Date
Expand Down
210 changes: 210 additions & 0 deletions lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* 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.content.Entity
import android.net.Uri
import at.bitfire.ical4android.Task
import at.bitfire.synctools.mapping.tasks.DmfsTaskBuilder
import at.bitfire.synctools.mapping.tasks.DmfsTaskProcessor
import at.bitfire.synctools.storage.BatchOperation
import at.bitfire.synctools.storage.LocalStorageException
import at.bitfire.synctools.storage.toContentValues
import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.property.RelatedTo
import org.dmfs.tasks.contract.TaskContract
import java.io.FileNotFoundException
import java.util.logging.Level
import java.util.logging.Logger

/**
* Stores and retrieves tasks to/from the tasks.org-content provider (currently tasks.org and
* OpenTasks).
*
* A task in the context of this class is one row in the [org.dmfs.tasks.contract.TaskContract.Tasks] table,
* plus associated data rows (like alarms and reminders).
*
* Exceptions (of recurring tasks) have their own entries in the [org.dmfs.tasks.contract.TaskContract.Tasks] table and thus
* are separate.
*
* The SEQUENCE field is stored in [org.dmfs.tasks.contract.TaskContract.CommonSyncColumns.SYNC_VERSION], so don't use [org.dmfs.tasks.contract.TaskContract.CommonSyncColumns.SYNC_VERSION]
* for anything else.
*
* @param taskList task list where the task is stored
* @param values entity with all columns, as returned by the calendar provider; [org.dmfs.tasks.contract.TaskContract.Tasks._ID] must be set to a non-null value
*/
class DmfsTask(
val taskList: DmfsTaskList,
val values: Entity
) {

private val logger = Logger.getLogger(javaClass.name)

private val mainValues
get() = values.entityValues

var id: Long = mainValues.getAsLong(TaskContract.Tasks._ID)
var syncId: String? = mainValues.getAsString(TaskContract.Tasks._SYNC_ID)
var eTag: String? = mainValues.getAsString(COLUMN_ETAG)
var flags: Int = mainValues.getAsInteger(COLUMN_FLAGS) ?: 0

var task: Task? = null
/**
* This getter returns the full task data, either from [task] or, if [task] is null, by reading task
* number [id] from the task provider
* @throws IllegalArgumentException if task has not been saved yet
* @throws java.io.FileNotFoundException if there's no task with [id] in the task provider
* @throws android.os.RemoteException on task provider errors
*/
get() {
if (field != null)
return field
val id = requireNotNull(id)

try {
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
val newTask = Task()
field = newTask

val values = cursor.toContentValues()
logger.log(Level.FINER, "Found task", values)
val processor = DmfsTaskProcessor(taskList)
processor.populateTask(values, newTask)

if (values.containsKey(TaskContract.Properties.PROPERTY_ID)) {
// process the first property, which is combined with the task row
processor.populateProperty(values, newTask)

while (cursor.moveToNext()) {
// process the other properties
processor.populateProperty(cursor.toContentValues(), newTask)
}
}

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

field = newTask
return newTask
}
}
} catch (e: Exception) {
/* Populating event has been interrupted by an exception, so we reset the event to
avoid an inconsistent state. This also ensures that the exception will be thrown
again on the next get() call. */
field = null
throw e
}
throw FileNotFoundException("Couldn't find task #$id")
}

/**
* Saves the unsaved [task] into the task provider storage.
*
* @return content URI of the created task
*
* @throws at.bitfire.synctools.storage.LocalStorageException when the tasks provider doesn't return a result row
* @throws android.os.RemoteException on tasks provider errors
*/
fun add(): Uri {
val batch = TasksBatchOperation(taskList.provider.client)

val requiredTask = requireNotNull(task)
val builder = DmfsTaskBuilder(taskList, requiredTask, id, syncId, eTag, flags)
val idxTask = builder.addRows(batch)
builder.insertProperties(batch, idxTask)

batch.commit()

val resultUri = batch.getResult(0)?.uri
?: throw LocalStorageException("Empty result from provider when adding a task")
id = ContentUris.parseId(resultUri)
return resultUri
}

/**
* Updates an already existing task in the tasks provider storage with the values
* from the instance.
*
* @return content URI of the updated task
*
* @throws LocalStorageException when the tasks provider doesn't return a result row
* @throws android.os.RemoteException on tasks provider errors
*/
fun update(task: Task): Uri {
this.task = task
val existingId = requireNotNull(id)

val batch = TasksBatchOperation(taskList.provider.client)

// remove associated rows which are added later again
batch += BatchOperation.CpoBuilder
.newDelete(taskList.tasksPropertiesUri())
.withSelection("${TaskContract.Properties.TASK_ID}=?", arrayOf(existingId.toString()))

// update task
val builder = DmfsTaskBuilder(taskList, task, id, syncId, eTag, flags)
builder.updateRows(batch)

// insert task properties again
builder.insertProperties(batch, null)

batch.commit()
return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(taskList.providerName.authority), existingId)
}

fun update(values: ContentValues) {
taskList.provider.client.update(taskSyncURI(), values, null, null)
}

/**
* Deletes an existing task from the tasks provider storage.
*
* @return number of affected rows
*
* @throws android.os.RemoteException on tasks provider errors
*/
fun delete(): Int {
return taskList.provider.client.delete(taskSyncURI(), null, null)
}

private fun taskSyncURI(loadProperties: Boolean = false): Uri {
val id = requireNotNull(id)
return ContentUris.withAppendedId(taskList.tasksUri(loadProperties), id)
}

companion object {
const val UNKNOWN_PROPERTY_DATA = TaskContract.Properties.DATA0

const val COLUMN_ETAG = TaskContract.Tasks.SYNC1

const val COLUMN_FLAGS = TaskContract.Tasks.SYNC2
}

}
Loading