From 94598b2d7bef6145df91ca7a4b17c04c990e500c Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 14:42:35 +0200 Subject: [PATCH 1/4] Implement StartTimeHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 16 +---- .../mapping/tasks/handler/StartTimeHandler.kt | 25 +++++++ .../mapping/tasks/handler/TaskTimeField.kt | 54 +++++++++++++++ .../tasks/handler/StartTimeHandlerTest.kt | 66 +++++++++++++++++++ 4 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandler.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandlerTest.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 67a4c220..2bbef85c 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.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler import at.bitfire.synctools.mapping.tasks.handler.SequenceHandler +import at.bitfire.synctools.mapping.tasks.handler.StartTimeHandler 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 @@ -23,7 +24,6 @@ import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.property.Clazz import net.fortuna.ical4j.model.property.Completed -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 @@ -57,6 +57,7 @@ class DmfsTaskProcessor( UidHandler(), TitleHandler(), SequenceHandler(), + StartTimeHandler(), ) private val propertyHandlers: Map = mapOf( @@ -126,19 +127,6 @@ class DmfsTaskProcessor( values.getAsLong(Tasks.CREATED)?.let { to.createdAt = it } values.getAsLong(Tasks.LAST_MODIFIED)?.let { to.lastModified = it } - values.getAsLong(Tasks.DTSTART)?.let { dtStart -> - val instant = Instant.ofEpochMilli(dtStart) - to.dtStart = - if (allDay) - DtStart(instant.toLocalDate()) - else { - if (tz == null) - DtStart(instant) - else - DtStart(instant.atZone(tz.toZoneId())) - } - } - values.getAsLong(Tasks.DUE)?.let { due -> val instant = Instant.ofEpochMilli(due) to.due = diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandler.kt new file mode 100644 index 00000000..fb4985a4 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandler.kt @@ -0,0 +1,25 @@ +/* + * 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.DtStart +import org.dmfs.tasks.contract.TaskContract.Tasks + +class StartTimeHandler : DmfsTaskFieldHandler { + + override fun process(from: ContentValues, to: Task) { + val epochMillis = from.getAsLong(Tasks.DTSTART) ?: return + + val allDay = (from.getAsInteger(Tasks.IS_ALLDAY) ?: 0) != 0 + val tzId = from.getAsString(Tasks.TZ) + + to.dtStart = DtStart(TaskTimeField(epochMillis, tzId, allDay).toTemporal()) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt new file mode 100644 index 00000000..46f29416 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt @@ -0,0 +1,54 @@ +/* + * 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 at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate +import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import java.time.Instant +import java.time.temporal.Temporal + +/** + * Converts a task timestamp (epoch milliseconds) together with its timezone and all-day flag + * into the appropriate [Temporal] type for use in iCalendar properties. + * + * Analogous to [at.bitfire.synctools.mapping.calendar.handler.AndroidTimeField] for calendar events. + * + * @param timestamp epoch milliseconds (value of [org.dmfs.tasks.contract.TaskContract.Tasks.DTSTART] or [org.dmfs.tasks.contract.TaskContract.Tasks.DUE]) + * @param tzId value of [org.dmfs.tasks.contract.TaskContract.Tasks.TZ]: + * `null` for all-day tasks storage; timezone ID (e.g. `"UTC"`, `"Europe/Berlin"`) + * for non-all-day tasks. + * @param allDay whether [org.dmfs.tasks.contract.TaskContract.Tasks.IS_ALLDAY] is non-zero + */ +class TaskTimeField( + private val timestamp: Long, + private val tzId: String?, + private val allDay: Boolean, +) { + + /** + * Converts the stored timestamp to the correct [Temporal] representation: + * - `allDay = true` → [java.time.LocalDate] (interpreted at UTC midnight) + * - `allDay = false`, no/unknown timezone → [Instant] (UTC) + * - `allDay = false`, known timezone → [java.time.ZonedDateTime] + */ + fun toTemporal(): Temporal { + val instant = Instant.ofEpochMilli(timestamp) + + if (allDay) + return instant.toLocalDate() + + val tz = tzId?.let { + TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(it) + } + + return if (tz == null) + instant + else + instant.atZone(tz.toZoneId()) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandlerTest.kt new file mode 100644 index 00000000..2eec3aa8 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/StartTimeHandlerTest.kt @@ -0,0 +1,66 @@ +/* + * 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.DtStart +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 +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime + +@RunWith(RobolectricTestRunner::class) +class StartTimeHandlerTest { + + private val handler = StartTimeHandler() + + @Test + fun `No DTSTART leaves dtStart null`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.dtStart) + } + + @Test + fun `All-day start time`() { + val task = Task() + handler.process(contentValuesOf( + Tasks.DTSTART to 1592697600000L, // 2020-06-21 00:00:00 UTC + Tasks.IS_ALLDAY to 1, + ), task) + assertEquals(DtStart(LocalDate.of(2020, 6, 21)), task.dtStart) + } + + @Test + fun `Non-all-day start time with timezone`() { + val task = Task() + handler.process(contentValuesOf( + Tasks.DTSTART to 1592733600000L, // 2020-06-21 10:00:00 UTC = 12:00:00 Europe/Vienna + Tasks.TZ to "Europe/Vienna", + ), task) + val expected = ZonedDateTime.of(2020, 6, 21, 12, 0, 0, 0, ZoneId.of("Europe/Vienna")) + assertEquals(DtStart(expected), task.dtStart) + } + + @Test + fun `Non-all-day start time without timezone (UTC Instant)`() { + val task = Task() + handler.process(contentValuesOf( + Tasks.DTSTART to 1592733600000L, // 2020-06-21 10:00:00 UTC + ), task) + assertEquals(DtStart(Instant.ofEpochMilli(1592733600000L)), task.dtStart) + } + +} From 542c1676e9556376b6de1196b2de50b5c992be34 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 14:45:10 +0200 Subject: [PATCH 2/4] Implement DueHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 24 +------ .../mapping/tasks/handler/DueHandler.kt | 25 +++++++ .../mapping/tasks/handler/DueHandlerTest.kt | 66 +++++++++++++++++++ 3 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandlerTest.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 2bbef85c..8aefa096 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 @@ -9,8 +9,8 @@ package at.bitfire.synctools.mapping.tasks import android.content.ContentValues 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.DueHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler import at.bitfire.synctools.mapping.tasks.handler.SequenceHandler @@ -20,11 +20,9 @@ import at.bitfire.synctools.mapping.tasks.handler.UidHandler 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.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.property.Clazz import net.fortuna.ical4j.model.property.Completed -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 @@ -58,6 +56,7 @@ class DmfsTaskProcessor( TitleHandler(), SequenceHandler(), StartTimeHandler(), + DueHandler(), ) private val propertyHandlers: Map = mapOf( @@ -118,28 +117,9 @@ class DmfsTaskProcessor( val allDay = (values.getAsInteger(Tasks.IS_ALLDAY) ?: 0) != 0 - val tzID = values.getAsString(Tasks.TZ) - val tz = tzID?.let { - val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - tzRegistry.getTimeZone(it) - } - values.getAsLong(Tasks.CREATED)?.let { to.createdAt = it } values.getAsLong(Tasks.LAST_MODIFIED)?.let { to.lastModified = it } - values.getAsLong(Tasks.DUE)?.let { due -> - val instant = Instant.ofEpochMilli(due) - to.due = - if (allDay) - Due(instant.toLocalDate()) - else { - if (tz == null) - Due(instant) - else - Due(instant.atZone(tz.toZoneId())) - } - } - values.getAsString(Tasks.DURATION)?.let { duration -> val fixedDuration = AndroidTimeUtils.parseDuration(duration) to.duration = Duration(fixedDuration) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandler.kt new file mode 100644 index 00000000..0e579c38 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandler.kt @@ -0,0 +1,25 @@ +/* + * 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.Due +import org.dmfs.tasks.contract.TaskContract.Tasks + +class DueHandler : DmfsTaskFieldHandler { + + override fun process(from: ContentValues, to: Task) { + val epochMillis = from.getAsLong(Tasks.DUE) ?: return + + val allDay = (from.getAsInteger(Tasks.IS_ALLDAY) ?: 0) != 0 + val tzId = from.getAsString(Tasks.TZ) + + to.due = Due(TaskTimeField(epochMillis, tzId, allDay).toTemporal()) + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandlerTest.kt new file mode 100644 index 00000000..b4b1dbbf --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DueHandlerTest.kt @@ -0,0 +1,66 @@ +/* + * 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.Due +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 +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime + +@RunWith(RobolectricTestRunner::class) +class DueHandlerTest { + + private val handler = DueHandler() + + @Test + fun `No DUE leaves due null`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.due) + } + + @Test + fun `All-day due date`() { + val task = Task() + handler.process(contentValuesOf( + Tasks.DUE to 1592697600000L, // 2020-06-21 00:00:00 UTC + Tasks.IS_ALLDAY to 1, + ), task) + assertEquals(Due(LocalDate.of(2020, 6, 21)), task.due) + } + + @Test + fun `Non-all-day due with timezone`() { + val task = Task() + handler.process(contentValuesOf( + Tasks.DUE to 1592733600000L, // 2020-06-21 10:00:00 UTC = 12:00:00 Europe/Vienna + Tasks.TZ to "Europe/Vienna", + ), task) + val expected = ZonedDateTime.of(2020, 6, 21, 12, 0, 0, 0, ZoneId.of("Europe/Vienna")) + assertEquals(Due(expected), task.due) + } + + @Test + fun `Non-all-day due without timezone (UTC Instant)`() { + val task = Task() + handler.process(contentValuesOf( + Tasks.DUE to 1592733600000L, // 2020-06-21 10:00:00 UTC + ), task) + assertEquals(Due(Instant.ofEpochMilli(1592733600000L)), task.due) + } + +} From 0e0944c8595994e8239f677ebc117909713960f0 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 14:46:15 +0200 Subject: [PATCH 3/4] Implement DurationHandler with unit test --- .../mapping/tasks/DmfsTaskProcessor.kt | 8 +--- .../mapping/tasks/handler/DurationHandler.kt | 23 +++++++++ .../tasks/handler/DurationHandlerTest.kt | 47 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandler.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandlerTest.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 8aefa096..9af05dd5 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.synctools.mapping.tasks.handler.AlarmsHandler import at.bitfire.synctools.mapping.tasks.handler.DueHandler +import at.bitfire.synctools.mapping.tasks.handler.DurationHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskFieldHandler import at.bitfire.synctools.mapping.tasks.handler.DmfsTaskPropertyHandler import at.bitfire.synctools.mapping.tasks.handler.SequenceHandler @@ -23,7 +24,6 @@ import at.bitfire.synctools.util.AndroidTimeUtils import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.property.Clazz import net.fortuna.ical4j.model.property.Completed -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 @@ -57,6 +57,7 @@ class DmfsTaskProcessor( SequenceHandler(), StartTimeHandler(), DueHandler(), + DurationHandler(), ) private val propertyHandlers: Map = mapOf( @@ -120,11 +121,6 @@ class DmfsTaskProcessor( values.getAsLong(Tasks.CREATED)?.let { to.createdAt = it } values.getAsLong(Tasks.LAST_MODIFIED)?.let { to.lastModified = it } - values.getAsString(Tasks.DURATION)?.let { duration -> - val fixedDuration = AndroidTimeUtils.parseDuration(duration) - to.duration = Duration(fixedDuration) - } - values.getAsString(Tasks.RDATE)?.let { rdateStr -> AndroidTimeUtils.androidStringToRecurrenceSet(rdateStr, allDay) { dates -> RDate(dates) }?.let { to.rDates += it } } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandler.kt new file mode 100644 index 00000000..3106a956 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandler.kt @@ -0,0 +1,23 @@ +/* + * 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 at.bitfire.synctools.util.AndroidTimeUtils +import net.fortuna.ical4j.model.property.Duration +import org.dmfs.tasks.contract.TaskContract.Tasks + +class DurationHandler : DmfsTaskFieldHandler { + + override fun process(from: ContentValues, to: Task) { + from.getAsString(Tasks.DURATION)?.let { durationStr -> + to.duration = Duration(AndroidTimeUtils.parseDuration(durationStr)) + } + } + +} diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandlerTest.kt new file mode 100644 index 00000000..5cd838a9 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/handler/DurationHandlerTest.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 at.bitfire.synctools.util.AndroidTimeUtils +import net.fortuna.ical4j.model.property.Duration +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 DurationHandlerTest { + + private val handler = DurationHandler() + + @Test + fun `No DURATION leaves duration null`() { + val task = Task() + handler.process(ContentValues(), task) + assertNull(task.duration) + } + + @Test + fun `DURATION PT1H is mapped correctly`() { + val task = Task() + handler.process(contentValuesOf(Tasks.DURATION to "PT1H"), task) + assertEquals(Duration(AndroidTimeUtils.parseDuration("PT1H")), task.duration) + } + + @Test + fun `DURATION P1D is mapped correctly`() { + val task = Task() + handler.process(contentValuesOf(Tasks.DURATION to "P1D"), task) + assertEquals(Duration(AndroidTimeUtils.parseDuration("P1D")), task.duration) + } + +} From 4b3cfd92a342399a0b08ae78f2d6f1de75946997 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Fri, 22 May 2026 15:15:05 +0200 Subject: [PATCH 4/4] Cache TimeZoneRegistry in TaskTimeField --- .../synctools/mapping/tasks/handler/TaskTimeField.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt index 46f29416..dadb607c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/handler/TaskTimeField.kt @@ -29,6 +29,8 @@ class TaskTimeField( private val allDay: Boolean, ) { + private val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() } + /** * Converts the stored timestamp to the correct [Temporal] representation: * - `allDay = true` → [java.time.LocalDate] (interpreted at UTC midnight) @@ -41,9 +43,7 @@ class TaskTimeField( if (allDay) return instant.toLocalDate() - val tz = tzId?.let { - TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(it) - } + val tz = tzId?.let { tzRegistry.getTimeZone(it) } return if (tz == null) instant