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
375 changes: 107 additions & 268 deletions lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.mapping.tasks.builder

import android.content.Entity
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.Task
import at.bitfire.synctools.storage.tasks.DmfsTaskList
import at.bitfire.synctools.util.AlarmTriggerCalculator
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.property.Action
import net.fortuna.ical4j.model.property.immutable.ImmutableAction
import net.fortuna.ical4j.model.parameter.Related
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
import java.util.Locale
import kotlin.jvm.optionals.getOrNull

class AlarmsBuilder(
private val taskList: DmfsTaskList
) : DmfsTaskFieldBuilder {

override fun build(from: Task, to: Entity) {
for (alarm in from.alarms) {
val (alarmRef, minutes) = AlarmTriggerCalculator.alarmTriggerToMinutes(
alarm = alarm,
refStart = from.dtStart,
refEnd = from.end,
allowRelEnd = true
) ?: continue

val ref = when (alarmRef) {
Related.END ->
Alarm.ALARM_REFERENCE_DUE_DATE
else /* Related.START is the default value */ ->
Alarm.ALARM_REFERENCE_START_DATE
}

val alarmType = when (
alarm.getProperty<Action>(Property.ACTION).getOrNull()?.value?.uppercase(Locale.ROOT)
) {
ImmutableAction.VALUE_AUDIO -> Alarm.ALARM_TYPE_SOUND
ImmutableAction.VALUE_DISPLAY -> Alarm.ALARM_TYPE_MESSAGE
ImmutableAction.VALUE_EMAIL -> Alarm.ALARM_TYPE_EMAIL
else -> Alarm.ALARM_TYPE_NOTHING
}

to.addSubValue(
taskList.tasksPropertiesUri(),
contentValuesOf(
Alarm.MIMETYPE to Alarm.CONTENT_ITEM_TYPE,
Alarm.MINUTES_BEFORE to minutes,
Alarm.REFERENCE to ref,
Alarm.MESSAGE to (alarm.description?.value ?: alarm.summary),
Alarm.ALARM_TYPE to alarmType
)
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.mapping.tasks.builder

import android.content.Entity
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.Task
import at.bitfire.synctools.storage.tasks.DmfsTaskList
import org.dmfs.tasks.contract.TaskContract.Property.Category

class CategoriesBuilder(
private val taskList: DmfsTaskList
) : DmfsTaskFieldBuilder {

override fun build(from: Task, to: Entity) {
for (category in from.categories) {
to.addSubValue(
taskList.tasksPropertiesUri(),
contentValuesOf(
Category.MIMETYPE to Category.CONTENT_ITEM_TYPE,
Category.CATEGORY_NAME to category
)
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.mapping.tasks.builder

import android.content.Entity
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.Task
import at.bitfire.synctools.storage.tasks.DmfsTaskList
import org.dmfs.tasks.contract.TaskContract.Property.Comment

class CommentsBuilder(
private val taskList: DmfsTaskList
) : DmfsTaskFieldBuilder {

override fun build(from: Task, to: Entity) {
val comment = from.comment ?: return
to.addSubValue(
taskList.tasksPropertiesUri(),
contentValuesOf(
Comment.MIMETYPE to Comment.CONTENT_ITEM_TYPE,
Comment.COMMENT to comment
)
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.mapping.tasks.builder

import android.content.Entity
import at.bitfire.ical4android.Task
import org.dmfs.tasks.contract.TaskContract.Tasks

class DirtyBuilder : DmfsTaskFieldBuilder {

override fun build(from: Task, to: Entity) {
// DIRTY is always unset when we create or update a task row
to.entityValues.put(Tasks._DIRTY, 0)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.mapping.tasks.builder

import android.content.Entity
import at.bitfire.ical4android.Task
import org.dmfs.tasks.contract.TaskContract.Tasks

class ListIdBuilder(
private val listId: Long
) : DmfsTaskFieldBuilder {

override fun build(from: Task, to: Entity) {
to.entityValues.put(Tasks.LIST_ID, listId)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.mapping.tasks.builder

import android.content.Entity
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.Task
import at.bitfire.synctools.storage.tasks.DmfsTaskList
import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.parameter.RelType
import org.dmfs.tasks.contract.TaskContract.Property.Relation
import org.dmfs.tasks.contract.TaskContract.Tasks
import kotlin.jvm.optionals.getOrNull

class RelationsBuilder(
private val taskList: DmfsTaskList
) : DmfsTaskFieldBuilder {

override fun build(from: Task, to: Entity) {
// parent_id will be re-calculated when the relation row is inserted (if there is any)
to.entityValues.put(Tasks.PARENT_ID, null as Long?)

for (relatedTo in from.relatedTo) {
val relType = when (relatedTo.getParameter<RelType>(Parameter.RELTYPE).getOrNull()) {
RelType.CHILD -> Relation.RELTYPE_CHILD
RelType.SIBLING -> Relation.RELTYPE_SIBLING
else /* RelType.PARENT, default value */ -> Relation.RELTYPE_PARENT
}
to.addSubValue(
taskList.tasksPropertiesUri(),
contentValuesOf(
Relation.MIMETYPE to Relation.CONTENT_ITEM_TYPE,
Relation.RELATED_UID to relatedTo.value,
Relation.RELATED_TYPE to relType
)
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.mapping.tasks.builder

import android.content.Entity
import androidx.core.content.contentValuesOf
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 org.dmfs.tasks.contract.TaskContract.Properties
import java.util.logging.Logger

class UnknownPropertiesBuilder(
private val taskList: DmfsTaskList
) : DmfsTaskFieldBuilder {

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

override fun build(from: Task, to: Entity) {
for (property in from.unknownProperties) {
if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) {
logger.warning("Ignoring unknown property with ${property.value.length} octets (too long)")
continue
}

to.addSubValue(
taskList.tasksPropertiesUri(),
contentValuesOf(
Properties.MIMETYPE to UnknownProperty.CONTENT_ITEM_TYPE,
UNKNOWN_PROPERTY_DATA to UnknownProperty.toJsonString(property)
)
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.mapping.tasks.builder

import android.content.ContentValues
import android.content.Entity
import android.net.Uri
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.Task
import at.bitfire.synctools.storage.tasks.DmfsTaskList
import at.bitfire.synctools.test.assertContentValuesEqual
import io.mockk.every
import io.mockk.mockk
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.property.Action
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.Trigger
import net.fortuna.ical4j.model.property.immutable.ImmutableAction
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.time.Duration
import java.time.ZoneOffset
import java.time.ZonedDateTime

@RunWith(RobolectricTestRunner::class)
class AlarmsBuilderTest {

private val propertiesUri = Uri.parse("content://org.dmfs.tasks/properties")
private val taskList = mockk<DmfsTaskList> {
every { tasksPropertiesUri() } returns propertiesUri
}
private val builder = AlarmsBuilder(taskList)

@Test
fun `No alarms`() {
val result = Entity(ContentValues())
builder.build(
from = Task(),
to = result
)
assertTrue(result.subValues.isEmpty())
}

@Test
fun `Audio alarm relative to start`() {
val result = Entity(ContentValues())
builder.build(
from = Task(
dtStart = DtStart(ZonedDateTime.of(2025, 1, 15, 10, 0, 0, 0, ZoneOffset.UTC))
).also {
it.alarms += VAlarm().also { alarm ->
alarm.add<VAlarm>(Action(ImmutableAction.VALUE_AUDIO))
alarm.add<VAlarm>(Trigger(Duration.ofMinutes(-15)))
}
},
to = result
)
assertEquals(1, result.subValues.size)
val values = result.subValues.first().values
assertContentValuesEqual(contentValuesOf(
Alarm.MIMETYPE to Alarm.CONTENT_ITEM_TYPE,
Alarm.MINUTES_BEFORE to 15,
Alarm.REFERENCE to Alarm.ALARM_REFERENCE_START_DATE,
Alarm.MESSAGE to null,
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_SOUND
), values)
assertEquals(propertiesUri, result.subValues.first().uri)
}

@Test
fun `Display alarm`() {
val result = Entity(ContentValues())
builder.build(
from = Task(
dtStart = DtStart(ZonedDateTime.of(2025, 1, 15, 10, 0, 0, 0, ZoneOffset.UTC))
).also {
it.alarms += VAlarm().also { alarm ->
alarm.add<VAlarm>(Action(ImmutableAction.VALUE_DISPLAY))
alarm.add<VAlarm>(Trigger(Duration.ofMinutes(-30)))
}
},
to = result
)
assertEquals(1, result.subValues.size)
assertContentValuesEqual(contentValuesOf(
Alarm.MIMETYPE to Alarm.CONTENT_ITEM_TYPE,
Alarm.MINUTES_BEFORE to 30,
Alarm.REFERENCE to Alarm.ALARM_REFERENCE_START_DATE,
Alarm.MESSAGE to null,
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_MESSAGE
), result.subValues.first().values)
}

}
Loading
Loading