Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

Commit c21a72f

Browse files
authored
Extract task field handlers title, UID and property alarms handler + tests (#401)
1 parent f5e0f50 commit c21a72f

9 files changed

Lines changed: 355 additions & 34 deletions

File tree

lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import android.content.ContentValues
1010
import at.bitfire.ical4android.Task
1111
import at.bitfire.ical4android.UnknownProperty
1212
import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate
13-
import at.bitfire.synctools.icalendar.propertyListOf
13+
import at.bitfire.synctools.mapping.tasks.handler.AlarmsHandler
14+
import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler
15+
import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler
16+
import at.bitfire.synctools.mapping.tasks.handler.TitleHandler
17+
import at.bitfire.synctools.mapping.tasks.handler.UidHandler
1418
import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA
1519
import at.bitfire.synctools.storage.tasks.DmfsTaskList
1620
import at.bitfire.synctools.util.AndroidTimeUtils
1721
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
18-
import net.fortuna.ical4j.model.component.VAlarm
1922
import net.fortuna.ical4j.model.parameter.RelType
20-
import net.fortuna.ical4j.model.parameter.Related
21-
import net.fortuna.ical4j.model.property.Action
2223
import net.fortuna.ical4j.model.property.Clazz
2324
import net.fortuna.ical4j.model.property.Completed
24-
import net.fortuna.ical4j.model.property.Description
2525
import net.fortuna.ical4j.model.property.DtStart
2626
import net.fortuna.ical4j.model.property.Due
2727
import net.fortuna.ical4j.model.property.Duration
@@ -32,7 +32,6 @@ import net.fortuna.ical4j.model.property.RDate
3232
import net.fortuna.ical4j.model.property.RRule
3333
import net.fortuna.ical4j.model.property.RelatedTo
3434
import net.fortuna.ical4j.model.property.Status
35-
import net.fortuna.ical4j.model.property.Trigger
3635
import org.dmfs.tasks.contract.TaskContract.Properties
3736
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
3837
import org.dmfs.tasks.contract.TaskContract.Property.Category
@@ -53,13 +52,23 @@ class DmfsTaskProcessor(
5352
private val taskList: DmfsTaskList
5453
) {
5554

55+
private val fieldHandlers: Array<DmfsTaskFieldHandler> = arrayOf(
56+
UidHandler(),
57+
TitleHandler(),
58+
)
59+
60+
private val propertyHandlers: Map<String, DmfsTaskPropertyHandler> = mapOf(
61+
Alarm.CONTENT_ITEM_TYPE to AlarmsHandler(),
62+
)
63+
5664
private val logger
5765
get() = Logger.getLogger(javaClass.name)
5866

5967
fun populateTask(values: ContentValues, to: Task) {
60-
to.uid = values.getAsString(Tasks._UID)
68+
for (handler in fieldHandlers)
69+
handler.process(values, to)
70+
6171
to.sequence = values.getAsInteger(Tasks.SYNC_VERSION)
62-
to.summary = values.getAsString(Tasks.TITLE)
6372
to.location = values.getAsString(Tasks.LOCATION)
6473
to.userAgents += taskList.providerName.packageName
6574

@@ -160,9 +169,14 @@ class DmfsTaskProcessor(
160169
fun populateProperty(row: ContentValues, to: Task) {
161170
logger.log(Level.FINER, "Found property", row)
162171

163-
when (val type = row.getAsString(Properties.MIMETYPE)) {
164-
Alarm.CONTENT_ITEM_TYPE ->
165-
populateAlarm(row, to)
172+
val type = row.getAsString(Properties.MIMETYPE)
173+
val handler = propertyHandlers[type]
174+
if (handler != null) {
175+
handler.process(row, to)
176+
return
177+
}
178+
179+
when (type) {
166180
Category.CONTENT_ITEM_TYPE ->
167181
to.categories += row.getAsString(Category.CATEGORY_NAME)
168182
Comment.CONTENT_ITEM_TYPE ->
@@ -176,29 +190,6 @@ class DmfsTaskProcessor(
176190
}
177191
}
178192

179-
private fun populateAlarm(row: ContentValues, to: Task) {
180-
val props = propertyListOf(
181-
Trigger(java.time.Duration.ofMinutes(-row.getAsLong(Alarm.MINUTES_BEFORE))).let {
182-
when (row.getAsInteger(Alarm.REFERENCE)) {
183-
Alarm.ALARM_REFERENCE_START_DATE -> it.add(Related.START)
184-
Alarm.ALARM_REFERENCE_DUE_DATE -> it.add(Related.END)
185-
else -> it
186-
}
187-
},
188-
Action(
189-
when (row.getAsInteger(Alarm.ALARM_TYPE)) {
190-
Alarm.ALARM_TYPE_EMAIL -> Action.VALUE_EMAIL
191-
Alarm.ALARM_TYPE_SOUND -> Action.VALUE_AUDIO
192-
// show alarm by default
193-
else -> Action.VALUE_DISPLAY
194-
}
195-
),
196-
Description(row.getAsString(Alarm.MESSAGE) ?: to.summary)
197-
)
198-
199-
to.alarms += VAlarm(props)
200-
}
201-
202193
private fun populateRelatedTo(row: ContentValues, to: Task) {
203194
val uid = row.getAsString(Relation.RELATED_UID)
204195
if (uid == null) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.tasks.handler
8+
9+
import android.content.ContentValues
10+
import at.bitfire.ical4android.Task
11+
import at.bitfire.synctools.icalendar.propertyListOf
12+
import net.fortuna.ical4j.model.component.VAlarm
13+
import net.fortuna.ical4j.model.parameter.Related
14+
import net.fortuna.ical4j.model.property.Action
15+
import net.fortuna.ical4j.model.property.Description
16+
import net.fortuna.ical4j.model.property.Trigger
17+
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
18+
19+
class AlarmsHandler : DmfsTaskPropertyHandler {
20+
21+
override fun process(row: ContentValues, to: Task) {
22+
val props = propertyListOf(
23+
Trigger(java.time.Duration.ofMinutes(-row.getAsLong(Alarm.MINUTES_BEFORE))).let {
24+
when (row.getAsInteger(Alarm.REFERENCE)) {
25+
Alarm.ALARM_REFERENCE_START_DATE -> it.add(Related.START)
26+
Alarm.ALARM_REFERENCE_DUE_DATE -> it.add(Related.END)
27+
else -> it
28+
}
29+
},
30+
Action(
31+
when (row.getAsInteger(Alarm.ALARM_TYPE)) {
32+
Alarm.ALARM_TYPE_EMAIL -> Action.VALUE_EMAIL
33+
Alarm.ALARM_TYPE_SOUND -> Action.VALUE_AUDIO
34+
// show alarm by default
35+
else -> Action.VALUE_DISPLAY
36+
}
37+
),
38+
Description(row.getAsString(Alarm.MESSAGE) ?: to.summary)
39+
)
40+
41+
to.alarms += VAlarm(props)
42+
}
43+
44+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.tasks.handler
8+
9+
import android.content.ContentValues
10+
import at.bitfire.ical4android.Task
11+
import at.bitfire.synctools.exception.InvalidLocalResourceException
12+
13+
interface DmfsTaskFieldHandler {
14+
15+
/**
16+
* Takes specific data from a task row (taken from the content provider) and maps it into
17+
* the given [Task].
18+
*
19+
* @param from task main row from the content provider
20+
* @param to destination object where the mapped data are stored
21+
*
22+
* @throws InvalidLocalResourceException on missing or invalid required fields
23+
*/
24+
fun process(from: ContentValues, to: Task)
25+
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.tasks.handler
8+
9+
import android.content.ContentValues
10+
import at.bitfire.ical4android.Task
11+
12+
interface DmfsTaskPropertyHandler {
13+
14+
/**
15+
* Takes specific data from a task property sub-row (taken from the content provider) and
16+
* maps it into the given [Task].
17+
*
18+
* Property sub-rows represent things like alarms, categories and relations.
19+
*
20+
* @param row property sub-row from the content provider
21+
* @param to destination object where the mapped data are stored
22+
*/
23+
fun process(row: ContentValues, to: Task)
24+
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.tasks.handler
8+
9+
import android.content.ContentValues
10+
import at.bitfire.ical4android.Task
11+
import org.dmfs.tasks.contract.TaskContract.Tasks
12+
13+
class TitleHandler : DmfsTaskFieldHandler {
14+
15+
override fun process(from: ContentValues, to: Task) {
16+
to.summary = from.getAsString(Tasks.TITLE)
17+
}
18+
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.tasks.handler
8+
9+
import android.content.ContentValues
10+
import at.bitfire.ical4android.Task
11+
import org.dmfs.tasks.contract.TaskContract.Tasks
12+
13+
class UidHandler : DmfsTaskFieldHandler {
14+
15+
override fun process(from: ContentValues, to: Task) {
16+
to.uid = from.getAsString(Tasks._UID)
17+
}
18+
19+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.tasks.handler
8+
9+
import androidx.core.content.contentValuesOf
10+
import at.bitfire.ical4android.Task
11+
import net.fortuna.ical4j.model.Property
12+
import net.fortuna.ical4j.model.component.VAlarm
13+
import net.fortuna.ical4j.model.property.Action
14+
import net.fortuna.ical4j.model.property.Trigger
15+
import net.fortuna.ical4j.model.property.immutable.ImmutableAction
16+
import org.dmfs.tasks.contract.TaskContract.Property.Alarm
17+
import org.junit.Assert.assertEquals
18+
import org.junit.Assert.assertTrue
19+
import org.junit.Test
20+
import org.junit.runner.RunWith
21+
import org.robolectric.RobolectricTestRunner
22+
import java.time.Duration
23+
import kotlin.jvm.optionals.getOrNull
24+
25+
@RunWith(RobolectricTestRunner::class)
26+
class AlarmsHandlerTest {
27+
28+
private val handler = AlarmsHandler()
29+
30+
@Test
31+
fun `Display alarm relative to start`() {
32+
val task = Task()
33+
handler.process(contentValuesOf(
34+
Alarm.MINUTES_BEFORE to 15L,
35+
Alarm.REFERENCE to Alarm.ALARM_REFERENCE_START_DATE,
36+
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_MESSAGE,
37+
), task)
38+
39+
assertEquals(1, task.alarms.size)
40+
val alarm = task.alarms.first()
41+
assertEquals(ImmutableAction.DISPLAY, alarm.actionProperty)
42+
assertEquals(Duration.ofMinutes(-15), alarm.triggerProperty.duration)
43+
}
44+
45+
@Test
46+
fun `Audio alarm relative to due`() {
47+
val task = Task()
48+
handler.process(contentValuesOf(
49+
Alarm.MINUTES_BEFORE to 10L,
50+
Alarm.REFERENCE to Alarm.ALARM_REFERENCE_DUE_DATE,
51+
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_SOUND,
52+
), task)
53+
54+
assertEquals(1, task.alarms.size)
55+
val alarm = task.alarms.first()
56+
assertEquals(ImmutableAction.AUDIO, alarm.actionProperty)
57+
assertEquals(Duration.ofMinutes(-10), alarm.triggerProperty.duration)
58+
// Related.END parameter should be set for due-relative alarms
59+
assertTrue(alarm.triggerProperty.getParameter<net.fortuna.ical4j.model.parameter.Related>(
60+
net.fortuna.ical4j.model.Parameter.RELATED
61+
).isPresent)
62+
}
63+
64+
@Test
65+
fun `Email alarm`() {
66+
val task = Task()
67+
handler.process(contentValuesOf(
68+
Alarm.MINUTES_BEFORE to 5L,
69+
Alarm.REFERENCE to Alarm.ALARM_REFERENCE_START_DATE,
70+
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_EMAIL,
71+
), task)
72+
73+
assertEquals(1, task.alarms.size)
74+
assertEquals(ImmutableAction.EMAIL, task.alarms.first().actionProperty)
75+
}
76+
77+
@Test
78+
fun `Alarm message is used as description`() {
79+
val task = Task()
80+
handler.process(contentValuesOf(
81+
Alarm.MINUTES_BEFORE to 0L,
82+
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_MESSAGE,
83+
Alarm.MESSAGE to "Don't forget!",
84+
), task)
85+
86+
assertEquals("Don't forget!", task.alarms.first().description?.value)
87+
}
88+
89+
@Test
90+
fun `Task summary used as description when no alarm message`() {
91+
val task = Task(summary = "Task Title")
92+
handler.process(contentValuesOf(
93+
Alarm.MINUTES_BEFORE to 0L,
94+
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_MESSAGE,
95+
), task)
96+
97+
assertEquals("Task Title", task.alarms.first().description?.value)
98+
}
99+
100+
@Test
101+
fun `Multiple alarms accumulate`() {
102+
val task = Task()
103+
handler.process(contentValuesOf(
104+
Alarm.MINUTES_BEFORE to 10L,
105+
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_MESSAGE,
106+
), task)
107+
handler.process(contentValuesOf(
108+
Alarm.MINUTES_BEFORE to 20L,
109+
Alarm.ALARM_TYPE to Alarm.ALARM_TYPE_SOUND,
110+
), task)
111+
112+
assertEquals(2, task.alarms.size)
113+
}
114+
115+
}
116+
117+
private val VAlarm.actionProperty: Action?
118+
get() = getProperty<Action>(Property.ACTION).getOrNull()
119+
120+
private val VAlarm.triggerProperty: Trigger
121+
get() = getProperty<Trigger>(Property.TRIGGER).get()

0 commit comments

Comments
 (0)