diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilder.kt index b8f6578e..87d018d1 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilder.kt @@ -8,13 +8,15 @@ package at.bitfire.synctools.mapping.tasks.builder import android.content.Entity import at.bitfire.ical4android.Task +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import org.dmfs.tasks.contract.TaskContract.Tasks class CompletedBuilder : DmfsTaskFieldBuilder { override fun build(from: Task, to: Entity) { // COMPLETED must always be a DATE-TIME - to.entityValues.put(Tasks.COMPLETED, from.completedAt?.date?.toEpochMilli()) + to.entityValues.put(Tasks.COMPLETED, from.completedAt?.normalizedDate()?.toTimestamp()) to.entityValues.put(Tasks.COMPLETED_IS_ALLDAY, 0) } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt index 3caff1b3..97999254 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt @@ -77,7 +77,7 @@ object AndroidTimeUtils { * Same as [toInstant], but returns a UNIX timestamp (in milliseconds) instead of an [Instant]. */ fun Temporal.toTimestamp(): Long = - toInstant().epochSecond * 1000 + toInstant().toEpochMilli() /** * Converts this [Temporal] to a [ZonedDateTime] that is created from the timestamp returned by diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilderTest.kt index 8c73802b..86892bd8 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/tasks/builder/CompletedBuilderTest.kt @@ -9,18 +9,24 @@ package at.bitfire.synctools.mapping.tasks.builder import android.content.ContentValues import android.content.Entity import androidx.core.content.contentValuesOf +import at.bitfire.DefaultTimezoneRule import at.bitfire.ical4android.Task import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.property.Completed import org.dmfs.tasks.contract.TaskContract.Tasks +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.Instant +import java.time.LocalDateTime @RunWith(RobolectricTestRunner::class) class CompletedBuilderTest { + @get:Rule + val tzRule = DefaultTimezoneRule("Europe/Berlin") + private val builder = CompletedBuilder() @Test @@ -50,4 +56,22 @@ class CompletedBuilderTest { ), result.entityValues) } + @Test + fun `COMPLETED is floating LocalDateTime`() { + // COMPLETED without timezone (floating) must not crash with ClassCastException + // A floating COMPLETED is represented as a string without 'Z' (e.g. "20240601T120000") + val result = Entity(ContentValues()) + builder.build( + from = Task(completedAt = Completed("20240601T120000")), + to = result + ) + // floating date-time is interpreted in system default timezone + val expectedTimestamp = LocalDateTime.of(2024, 6, 1, 12, 0, 0) + .atZone(tzRule.defaultZoneId).toInstant().toEpochMilli() + assertContentValuesEqual(contentValuesOf( + Tasks.COMPLETED to expectedTimestamp, + Tasks.COMPLETED_IS_ALLDAY to 0 + ), result.entityValues) + } + }