From be419139feff5b1aff0a62880bd2a9216500ca82 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 12:49:26 +0200 Subject: [PATCH 1/8] Implement DescriptionHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 3 +- .../tasks/handler/DescriptionHandler.kt | 19 ++++++++++ .../tasks/handler/DescriptionHandlerTest.kt | 38 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandlerTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index eeb00e7f..883687f0 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -11,6 +11,7 @@ import at.bitfire.ical4android.Task import at.bitfire.ical4android.UnknownProperty import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.synctools.mapping.tasks.handler.AlarmsHandler +import at.bitfire.synctools.mapping.tasks.handler.DescriptionHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler import at.bitfire.synctools.mapping.tasks.handler.TitleHandler @@ -55,6 +56,7 @@ class DmfsTaskProcessor( private val fieldHandlers: Array = arrayOf( UidHandler(), TitleHandler(), + DescriptionHandler(), ) private val propertyHandlers: Map = mapOf( @@ -81,7 +83,6 @@ class DmfsTaskProcessor( } } - to.description = values.getAsString(Tasks.DESCRIPTION) to.color = values.getAsInteger(Tasks.TASK_COLOR) to.url = values.getAsString(Tasks.URL) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandler.kt new file mode 100644 index 00000000..c0e227aa --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandler.kt @@ -0,0 +1,19 @@ +/* + * 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.handler + +import android.content.ContentValues +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks + +class DescriptionHandler : DmfsTaskFieldHandler { + + override fun process(from: ContentValues, to: Task) { + to.description = from.getAsString(Tasks.DESCRIPTION) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandlerTest.kt new file mode 100644 index 00000000..5259b804 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DescriptionHandlerTest.kt @@ -0,0 +1,38 @@ +/* + * 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.handler + +import android.content.ContentValues +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DescriptionHandlerTest { + + private val handler = DescriptionHandler() + + @Test + fun `No DESCRIPTION`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.description) + } + + @Test + fun `DESCRIPTION set`() { + val task = Task() + handler.process(contentValuesOf(Tasks.DESCRIPTION to "Task details"), task) + assertEquals("Task details", task.description) + } + +} From 6bbdc46ea40fcf2457307046604c11be76adf447 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 12:49:58 +0200 Subject: [PATCH 2/8] Implement LocationHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 3 +- .../mapping/tasks/handler/LocationHandler.kt | 19 ++++++++++ .../tasks/handler/LocationHandlerTest.kt | 38 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandlerTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index 883687f0..75d4910a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -14,6 +14,7 @@ import at.bitfire.synctools.mapping.tasks.handler.AlarmsHandler import at.bitfire.synctools.mapping.tasks.handler.DescriptionHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler +import at.bitfire.synctools.mapping.tasks.handler.LocationHandler import at.bitfire.synctools.mapping.tasks.handler.TitleHandler import at.bitfire.synctools.mapping.tasks.handler.UidHandler import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA @@ -57,6 +58,7 @@ class DmfsTaskProcessor( UidHandler(), TitleHandler(), DescriptionHandler(), + LocationHandler(), ) private val propertyHandlers: Map = mapOf( @@ -71,7 +73,6 @@ class DmfsTaskProcessor( handler.process(values, to) to.sequence = values.getAsInteger(Tasks.SYNC_VERSION) - to.location = values.getAsString(Tasks.LOCATION) to.userAgents += taskList.providerName.packageName values.getAsString(Tasks.GEO)?.takeIf { it.contains(",") }?.let { geo -> diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandler.kt new file mode 100644 index 00000000..d0459e95 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandler.kt @@ -0,0 +1,19 @@ +/* + * 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.handler + +import android.content.ContentValues +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks + +class LocationHandler : DmfsTaskFieldHandler { + + override fun process(from: ContentValues, to: Task) { + to.location = from.getAsString(Tasks.LOCATION) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandlerTest.kt new file mode 100644 index 00000000..7c5eb194 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/LocationHandlerTest.kt @@ -0,0 +1,38 @@ +/* + * 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.handler + +import android.content.ContentValues +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LocationHandlerTest { + + private val handler = LocationHandler() + + @Test + fun `No LOCATION`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.location) + } + + @Test + fun `LOCATION set`() { + val task = Task() + handler.process(contentValuesOf(Tasks.LOCATION to "Vienna, Austria"), task) + assertEquals("Vienna, Austria", task.location) + } + +} From 8a14e8f8db6453b23e64e44db218f22477451c96 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 12:50:39 +0200 Subject: [PATCH 3/8] Implement GeoHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 11 +--- .../mapping/tasks/handler/GeoHandler.kt | 31 +++++++++++ .../mapping/tasks/handler/GeoHandlerTest.kt | 53 +++++++++++++++++++ 3 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index 75d4910a..bb013166 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -14,6 +14,7 @@ import at.bitfire.synctools.mapping.tasks.handler.AlarmsHandler import at.bitfire.synctools.mapping.tasks.handler.DescriptionHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler +import at.bitfire.synctools.mapping.tasks.handler.GeoHandler import at.bitfire.synctools.mapping.tasks.handler.LocationHandler import at.bitfire.synctools.mapping.tasks.handler.TitleHandler import at.bitfire.synctools.mapping.tasks.handler.UidHandler @@ -59,6 +60,7 @@ class DmfsTaskProcessor( TitleHandler(), DescriptionHandler(), LocationHandler(), + GeoHandler(), ) private val propertyHandlers: Map = mapOf( @@ -75,15 +77,6 @@ class DmfsTaskProcessor( to.sequence = values.getAsInteger(Tasks.SYNC_VERSION) to.userAgents += taskList.providerName.packageName - values.getAsString(Tasks.GEO)?.takeIf { it.contains(",") }?.let { geo -> - val (lng, lat) = geo.split(',') - try { - to.geoPosition = Geo(lat.toBigDecimal(), lng.toBigDecimal()) - } catch (e: NumberFormatException) { - logger.log(Level.WARNING, "Invalid GEO value: $geo", e) - } - } - to.color = values.getAsInteger(Tasks.TASK_COLOR) to.url = values.getAsString(Tasks.URL) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt new file mode 100644 index 00000000..82d8c519 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt @@ -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.handler + +import android.content.ContentValues +import at.bitfire.ical4android.Task +import net.fortuna.ical4j.model.property.Geo +import org.dmfs.tasks.contract.TaskContract.Tasks +import java.util.logging.Level +import java.util.logging.Logger + +class GeoHandler : DmfsTaskFieldHandler { + + private val logger + get() = Logger.getLogger(javaClass.name) + + override fun process(from: ContentValues, to: Task) { + val geo = from.getAsString(Tasks.GEO)?.takeIf { it.contains(",") } ?: return + val (lng, lat) = geo.split(',') + try { + to.geoPosition = Geo(lat.toBigDecimal(), lng.toBigDecimal()) + } catch (e: NumberFormatException) { + logger.log(Level.WARNING, "Invalid GEO value: $geo", e) + } + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt new file mode 100644 index 00000000..d92ef46d --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt @@ -0,0 +1,53 @@ +/* + * 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.handler + +import android.content.ContentValues +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import net.fortuna.ical4j.model.property.Geo +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class GeoHandlerTest { + + private val handler = GeoHandler() + + @Test + fun `No GEO`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.geoPosition) + } + + @Test + fun `GEO is set`() { + val task = Task() + handler.process(contentValuesOf(Tasks.GEO to "16.3,48.2"), task) + assertEquals(Geo(48.2.toBigDecimal(), 16.3.toBigDecimal()), task.geoPosition) + } + + @Test + fun `GEO without comma is ignored`() { + val task = Task() + handler.process(contentValuesOf(Tasks.GEO to "invalid"), task) + assertNull(task.geoPosition) + } + + @Test + fun `GEO with invalid number is ignored`() { + val task = Task() + handler.process(contentValuesOf(Tasks.GEO to "not,a-number"), task) + assertNull(task.geoPosition) + } + +} From 276137d2ab6163cc550dce678e21808e1403e6c8 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 12:51:09 +0200 Subject: [PATCH 4/8] Implement ColorHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 3 +- .../mapping/tasks/handler/ColorHandler.kt | 19 ++++++++++ .../mapping/tasks/handler/ColorHandlerTest.kt | 38 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandlerTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index bb013166..2d30aa6b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -11,6 +11,7 @@ import at.bitfire.ical4android.Task import at.bitfire.ical4android.UnknownProperty import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.synctools.mapping.tasks.handler.AlarmsHandler +import at.bitfire.synctools.mapping.tasks.handler.ColorHandler import at.bitfire.synctools.mapping.tasks.handler.DescriptionHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler @@ -61,6 +62,7 @@ class DmfsTaskProcessor( DescriptionHandler(), LocationHandler(), GeoHandler(), + ColorHandler(), ) private val propertyHandlers: Map = mapOf( @@ -77,7 +79,6 @@ class DmfsTaskProcessor( to.sequence = values.getAsInteger(Tasks.SYNC_VERSION) to.userAgents += taskList.providerName.packageName - to.color = values.getAsInteger(Tasks.TASK_COLOR) to.url = values.getAsString(Tasks.URL) values.getAsString(Tasks.ORGANIZER)?.let { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandler.kt new file mode 100644 index 00000000..f8f8e8af --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandler.kt @@ -0,0 +1,19 @@ +/* + * 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.handler + +import android.content.ContentValues +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks + +class ColorHandler : DmfsTaskFieldHandler { + + override fun process(from: ContentValues, to: Task) { + to.color = from.getAsInteger(Tasks.TASK_COLOR) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandlerTest.kt new file mode 100644 index 00000000..77b88bdf --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/ColorHandlerTest.kt @@ -0,0 +1,38 @@ +/* + * 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.handler + +import android.content.ContentValues +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ColorHandlerTest { + + private val handler = ColorHandler() + + @Test + fun `No COLOR`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.color) + } + + @Test + fun `COLOR set`() { + val task = Task() + handler.process(contentValuesOf(Tasks.TASK_COLOR to 0xFF112233.toInt()), task) + assertEquals(0xFF112233.toInt(), task.color) + } + +} From 3e13a69f8d501a039f38304e2d101126c84fd46f Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 12:51:45 +0200 Subject: [PATCH 5/8] Implement UrlHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 4 +- .../mapping/tasks/handler/UrlHandler.kt | 19 ++++++++++ .../mapping/tasks/handler/UrlHandlerTest.kt | 38 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandlerTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index 2d30aa6b..a7ab6e63 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -19,6 +19,7 @@ import at.bitfire.synctools.mapping.tasks.handler.GeoHandler import at.bitfire.synctools.mapping.tasks.handler.LocationHandler import at.bitfire.synctools.mapping.tasks.handler.TitleHandler import at.bitfire.synctools.mapping.tasks.handler.UidHandler +import at.bitfire.synctools.mapping.tasks.handler.UrlHandler import at.bitfire.synctools.storage.tasks.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.synctools.util.AndroidTimeUtils @@ -63,6 +64,7 @@ class DmfsTaskProcessor( LocationHandler(), GeoHandler(), ColorHandler(), + UrlHandler(), ) private val propertyHandlers: Map = mapOf( @@ -79,8 +81,6 @@ class DmfsTaskProcessor( to.sequence = values.getAsInteger(Tasks.SYNC_VERSION) to.userAgents += taskList.providerName.packageName - to.url = values.getAsString(Tasks.URL) - values.getAsString(Tasks.ORGANIZER)?.let { try { to.organizer = Organizer("mailto:$it") diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandler.kt new file mode 100644 index 00000000..65f0fb21 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandler.kt @@ -0,0 +1,19 @@ +/* + * 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.handler + +import android.content.ContentValues +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks + +class UrlHandler : DmfsTaskFieldHandler { + + override fun process(from: ContentValues, to: Task) { + to.url = from.getAsString(Tasks.URL) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandlerTest.kt new file mode 100644 index 00000000..a6160c07 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/UrlHandlerTest.kt @@ -0,0 +1,38 @@ +/* + * 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.handler + +import android.content.ContentValues +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UrlHandlerTest { + + private val handler = UrlHandler() + + @Test + fun `No URL`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.url) + } + + @Test + fun `URL set`() { + val task = Task() + handler.process(contentValuesOf(Tasks.URL to "https://example.com"), task) + assertEquals("https://example.com", task.url) + } + +} From 0a63be81b6aed2d37779eb0e19195e00d8a7c559 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 12:52:47 +0200 Subject: [PATCH 6/8] Implement OrganizerHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 13 +---- .../mapping/tasks/handler/OrganizerHandler.kt | 31 ++++++++++++ .../tasks/handler/OrganizerHandlerTest.kt | 47 +++++++++++++++++++ 3 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index a7ab6e63..cce3028e 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -17,6 +17,7 @@ import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler import at.bitfire.synctools.mapping.tasks.handler.GeoHandler import at.bitfire.synctools.mapping.tasks.handler.LocationHandler +import at.bitfire.synctools.mapping.tasks.handler.OrganizerHandler import at.bitfire.synctools.mapping.tasks.handler.TitleHandler import at.bitfire.synctools.mapping.tasks.handler.UidHandler import at.bitfire.synctools.mapping.tasks.handler.UrlHandler @@ -31,8 +32,6 @@ import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due import net.fortuna.ical4j.model.property.Duration import net.fortuna.ical4j.model.property.ExDate -import net.fortuna.ical4j.model.property.Geo -import net.fortuna.ical4j.model.property.Organizer import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RelatedTo @@ -43,7 +42,6 @@ import org.dmfs.tasks.contract.TaskContract.Property.Category import org.dmfs.tasks.contract.TaskContract.Property.Comment import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.Tasks -import java.net.URISyntaxException import java.time.Instant import java.time.temporal.Temporal import java.util.logging.Level @@ -65,6 +63,7 @@ class DmfsTaskProcessor( GeoHandler(), ColorHandler(), UrlHandler(), + OrganizerHandler(), ) private val propertyHandlers: Map = mapOf( @@ -81,14 +80,6 @@ class DmfsTaskProcessor( to.sequence = values.getAsInteger(Tasks.SYNC_VERSION) to.userAgents += taskList.providerName.packageName - values.getAsString(Tasks.ORGANIZER)?.let { - try { - to.organizer = Organizer("mailto:$it") - } catch(e: URISyntaxException) { - logger.log(Level.WARNING, "Invalid ORGANIZER email", e) - } - } - values.getAsInteger(Tasks.PRIORITY)?.let { to.priority = it } // Note: big method – maybe split? Depends on how we want to proceed with refactoring. diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandler.kt new file mode 100644 index 00000000..fcee2b3e --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandler.kt @@ -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.handler + +import android.content.ContentValues +import at.bitfire.ical4android.Task +import net.fortuna.ical4j.model.property.Organizer +import org.dmfs.tasks.contract.TaskContract.Tasks +import java.net.URISyntaxException +import java.util.logging.Level +import java.util.logging.Logger + +class OrganizerHandler : DmfsTaskFieldHandler { + + private val logger + get() = Logger.getLogger(javaClass.name) + + override fun process(from: ContentValues, to: Task) { + val email = from.getAsString(Tasks.ORGANIZER) ?: return + try { + to.organizer = Organizer("mailto:$email") + } catch (e: URISyntaxException) { + logger.log(Level.WARNING, "Invalid ORGANIZER email", e) + } + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt new file mode 100644 index 00000000..58885329 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt @@ -0,0 +1,47 @@ +/* + * 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.handler + +import android.content.ContentValues +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Task +import net.fortuna.ical4j.model.property.Organizer +import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class OrganizerHandlerTest { + + private val handler = OrganizerHandler() + + @Test + fun `No ORGANIZER`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.organizer) + } + + @Test + fun `ORGANIZER is email address`() { + val task = Task() + handler.process(contentValuesOf(Tasks.ORGANIZER to "organizer@example.com"), task) + assertEquals(Organizer("mailto:organizer@example.com"), task.organizer) + } + + @Test + fun `ORGANIZER with invalid URI is ignored`() { + val task = Task() + // A string that causes URISyntaxException when prepended with "mailto:" + handler.process(contentValuesOf(Tasks.ORGANIZER to "not a valid\u0000email"), task) + assertNull(task.organizer) + } + +} From 844b0e9a6764c0aeca268d140f5cb13ada7f9c1b Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 13:07:03 +0200 Subject: [PATCH 7/8] Fix GeoHandler to parse GEO defensively --- .../bitfire/synctools/mapping/tasks/handler/GeoHandler.kt | 7 +++++-- .../synctools/mapping/tasks/handler/GeoHandlerTest.kt | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt index 82d8c519..3483cffd 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandler.kt @@ -19,8 +19,11 @@ class GeoHandler : DmfsTaskFieldHandler { get() = Logger.getLogger(javaClass.name) override fun process(from: ContentValues, to: Task) { - val geo = from.getAsString(Tasks.GEO)?.takeIf { it.contains(",") } ?: return - val (lng, lat) = geo.split(',') + val geo = from.getAsString(Tasks.GEO) ?: return + val commaIdx = geo.indexOf(',') + if (commaIdx < 0) return + val lng = geo.substring(0, commaIdx) + val lat = geo.substring(commaIdx + 1) try { to.geoPosition = Geo(lat.toBigDecimal(), lng.toBigDecimal()) } catch (e: NumberFormatException) { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt index d92ef46d..0658e032 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/GeoHandlerTest.kt @@ -36,6 +36,13 @@ class GeoHandlerTest { assertEquals(Geo(48.2.toBigDecimal(), 16.3.toBigDecimal()), task.geoPosition) } + @Test + fun `GEO with trailing comma is ignored`() { + val task = Task() + handler.process(contentValuesOf(Tasks.GEO to "16.3,"), task) + assertNull(task.geoPosition) + } + @Test fun `GEO without comma is ignored`() { val task = Task() From 2b037858c59d55d9aaf6a38c697bbd4b0dc42b18 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 13:12:52 +0200 Subject: [PATCH 8/8] Remove untestable invalid-URI test from OrganizerHandlerTest --- .../mapping/tasks/handler/OrganizerHandlerTest.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt index 58885329..d5bfe96d 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/OrganizerHandlerTest.kt @@ -36,12 +36,4 @@ class OrganizerHandlerTest { assertEquals(Organizer("mailto:organizer@example.com"), task.organizer) } - @Test - fun `ORGANIZER with invalid URI is ignored`() { - val task = Task() - // A string that causes URISyntaxException when prepended with "mailto:" - handler.process(contentValuesOf(Tasks.ORGANIZER to "not a valid\u0000email"), task) - assertNull(task.organizer) - } - }