diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt index c57e6d4c..c54a9db6 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt @@ -18,8 +18,8 @@ import at.bitfire.ical4android.Task import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.UnknownProperty import at.bitfire.ical4android.impl.TestTaskList -import at.bitfire.ical4android.util.DateUtils.toEpochMilli import at.bitfire.synctools.storage.tasks.DmfsTaskList +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.Email @@ -440,7 +440,7 @@ class DmfsTaskBuilderTest ( LocalDateTime.of( LocalDate.of(2020, 7, 3), LocalTime.of(1, 2, 3) - ).toEpochMilli(), + ).toTimestamp(), result.getAsLong(TaskContract.Tasks.DTSTART) ) assertEquals( @@ -476,7 +476,7 @@ class DmfsTaskBuilderTest ( LocalDateTime.of( LocalDate.of(2020, 7, 3), LocalTime.of(1, 2, 3) - ).toEpochMilli(), + ).toTimestamp(), result.getAsLong(TaskContract.Tasks.DUE) ) assertEquals( diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt index ae69374a..0fe55d62 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt @@ -11,7 +11,7 @@ import at.bitfire.synctools.exception.InvalidICalendarException import at.bitfire.synctools.icalendar.Css3Color import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.ICalendarParser -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.component.VToDo diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt index 2951de43..acc8bdfe 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt @@ -11,7 +11,7 @@ import at.bitfire.ical4android.ICalendar.Companion.withUserAgents import at.bitfire.synctools.icalendar.Css3Color import at.bitfire.synctools.icalendar.VTimeZoneMinifier import at.bitfire.synctools.icalendar.plusAssign -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Parameter diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index 69d7f9cb..ec5a6d05 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -8,15 +8,6 @@ package at.bitfire.ical4android.util import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.property.DateProperty -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.temporal.ChronoField -import java.time.temporal.ChronoUnit import java.time.temporal.Temporal /** @@ -57,39 +48,4 @@ object DateUtils { fun isDateTime(date: Temporal?): Boolean = date != null && TemporalAdapter.isDateTimePrecision(date) - /** - * Converts the given [Instant] by truncating it to days, and converting into [LocalDate] by its - * epoch timestamp. - */ - fun Instant.toLocalDate(): LocalDate { - val epochSeconds = truncatedTo(ChronoUnit.DAYS).epochSecond - return LocalDate.ofEpochDay(epochSeconds / (24 * 60 * 60 /*seconds in a day*/)) - } - - /** - * Converts the given generic [Temporal] into milliseconds since epoch. - * @param fallbackTimezone Any specific timezone to use as fallback if there's not enough - * information on the [Temporal] type (local types). Defaults to UTC. - * @throws IllegalArgumentException if the [Temporal] is from an unknown time, which also doesn't - * support [ChronoField.INSTANT_SECONDS] - */ - fun Temporal.toEpochMilli(fallbackTimezone: ZoneId? = null): Long { - // If the temporal supports instant seconds, we can compute epoch millis directly from them - if (isSupported(ChronoField.INSTANT_SECONDS)) { - val seconds = getLong(ChronoField.INSTANT_SECONDS) - val nanos = get(ChronoField.NANO_OF_SECOND) - // Convert seconds and nanos to millis - return (seconds * 1000) + (nanos / 1_000_000) - } - - return when (this) { - is Instant -> this.toEpochMilli() - is ZonedDateTime -> this.toInstant().toEpochMilli() - is OffsetDateTime -> this.toInstant().toEpochMilli() - is LocalDate -> this.atStartOfDay(fallbackTimezone ?: ZoneOffset.UTC).toInstant().toEpochMilli() - is LocalDateTime -> this.atZone(fallbackTimezone ?: ZoneOffset.UTC).toInstant().toEpochMilli() - else -> throw IllegalArgumentException("${this::class.java.simpleName} cannot be converted to epoch millis.") - } - } - } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt index b96e52aa..8a59e260 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt @@ -13,7 +13,6 @@ import at.bitfire.synctools.icalendar.AssociatedEvents import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.synctools.icalendar.recurrenceId -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime import at.bitfire.synctools.mapping.calendar.handler.AccessLevelHandler import at.bitfire.synctools.mapping.calendar.handler.AndroidEventFieldHandler import at.bitfire.synctools.mapping.calendar.handler.AttendeesHandler @@ -37,6 +36,7 @@ import at.bitfire.synctools.mapping.calendar.handler.UnknownPropertiesHandler import at.bitfire.synctools.mapping.calendar.handler.UrlHandler import at.bitfire.synctools.storage.calendar.EventAndExceptions import at.bitfire.synctools.storage.calendar.EventsContract +import at.bitfire.synctools.util.AndroidTimeUtils.toZonedDateTime import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.ParameterList import net.fortuna.ical4j.model.Property diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidRecurrenceMapper.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidRecurrenceMapper.kt index 8b8fa309..cb50aeca 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidRecurrenceMapper.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidRecurrenceMapper.kt @@ -7,8 +7,8 @@ package at.bitfire.synctools.mapping.calendar.builder import at.bitfire.ical4android.util.DateUtils -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.toZonedDateTime import net.fortuna.ical4j.model.Property import java.time.Instant import java.time.LocalDate diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapper.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapper.kt deleted file mode 100644 index 3ed70ba2..00000000 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapper.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.calendar.builder - -import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate -import net.fortuna.ical4j.model.TemporalAdapter -import net.fortuna.ical4j.util.TimeZones -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.temporal.Temporal - -object AndroidTemporalMapper { - - private const val TZID_UTC = "UTC" - - /** - * Converts this [Temporal] to the timestamp that should be used when writing an event to the - * Android calendar provider. - */ - fun Temporal.toTimestamp(): Long { - val epochSeconds = when (this) { - is LocalDate -> atStartOfDay().atZone(TimeZones.getDateTimeZone().toZoneId()).toEpochSecond() - is LocalDateTime -> atZone(TimeZones.getDefault().toZoneId()).toEpochSecond() - is OffsetDateTime -> toEpochSecond() - is ZonedDateTime -> toEpochSecond() - is Instant -> epochSecond - else -> error("Unsupported Temporal type: ${this::class.qualifiedName}") - } - - return epochSeconds * 1000L - } - - /** - * Converts this [Temporal] to a [ZonedDateTime] that is created from the timestamp returned by - * [toTimestamp] and the time zone returned by [androidTimezoneId]. - */ - fun Temporal.toZonedDateTime(): ZonedDateTime { - return Instant.ofEpochMilli(toTimestamp()).atZone(ZoneId.of(androidTimezoneId())) - } - - /** - * Returns the timezone ID that should be used when writing an event to the Android calendar provider. - * - * Note: For date-times with a given time zone, it needs to be a system time zone. Call - * [at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate] on dates coming from - * ical4j before calling this function. - * - * @return - "UTC" for dates and UTC date-times - * - the specified time zone ID for date-times with given time zone - * - the currently set default time zone ID for floating date-times - */ - fun Temporal.androidTimezoneId(): String { - return if (TemporalAdapter.isDateTimePrecision(this)) { - if (TemporalAdapter.isUtc(this)) { - TZID_UTC - } else if (TemporalAdapter.isFloating(this)) { - ZoneId.systemDefault().id - } else { - require(this is ZonedDateTime) { "Non-floating date-time must be a ZonedDateTime" } - - val timezoneId = this.zone.id - require(!timezoneId.startsWith("ical4j")) { - "ical4j ZoneIds are not supported. Call DatePropertyTzMapper.normalizedDate() " + - "before passing a date to this function." - } - - timezoneId - } - } else { - // For all-day events EventsColumns.EVENT_TIMEZONE must be "UTC". - TZID_UTC - } - } - -} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt index ffaabe7e..4f224499 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt @@ -16,7 +16,7 @@ import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.requireDtStart -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.RDate diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt index 4a9c8492..eb5482ed 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt @@ -13,9 +13,9 @@ import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.TimeApiExtensions.abs import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.requireDtStart -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.androidTimezoneId -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime +import at.bitfire.synctools.util.AndroidTimeUtils.androidTimezoneId +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.toZonedDateTime import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.RDate diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt index 9743820b..0c3e6419 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt @@ -12,8 +12,8 @@ import at.bitfire.ical4android.util.DateUtils import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.recurrenceId import at.bitfire.synctools.icalendar.requireDtStart -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.toZonedDateTime import net.fortuna.ical4j.model.component.VEvent import java.time.LocalDate import java.time.ZonedDateTime diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt index 15ea9822..677f86fa 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt @@ -10,8 +10,8 @@ import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.requireDtStart -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.androidTimezoneId -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.androidTimezoneId +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import net.fortuna.ical4j.model.component.VEvent import java.time.temporal.Temporal diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt index 844f21ba..d274aa91 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt @@ -10,8 +10,8 @@ import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.ical4android.util.TimeApiExtensions.abs import at.bitfire.synctools.icalendar.plusAssign -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime import at.bitfire.synctools.util.AndroidTimeUtils +import at.bitfire.synctools.util.AndroidTimeUtils.toZonedDateTime import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtEnd import java.time.Instant diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt index 50f06a50..155615d9 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt @@ -9,11 +9,11 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.ical4android.util.DateUtils -import at.bitfire.ical4android.util.DateUtils.toEpochMilli import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.synctools.exception.InvalidLocalResourceException -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime import at.bitfire.synctools.util.AndroidTimeUtils +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp +import at.bitfire.synctools.util.AndroidTimeUtils.toZonedDateTime import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.ExDate @@ -60,7 +60,7 @@ class RecurrenceFieldsHandler: AndroidEventFieldHandler { rule.recur = alignUntil(rule.recur, startDate) // skip if UNTIL is before event's DTSTART - val tsUntil = rule.recur.until?.toEpochMilli() + val tsUntil = rule.recur.until?.toTimestamp() if (tsUntil != null && tsUntil <= tsStart) { logger.warning("Ignoring $rule because UNTIL ($tsUntil) is not after DTSTART ($tsStart)") continue @@ -96,7 +96,7 @@ class RecurrenceFieldsHandler: AndroidEventFieldHandler { rule.recur = alignUntil(rule.recur, startDate) // skip if UNTIL is before event's DTSTART - val tsUntil = rule.recur.until?.toEpochMilli() + val tsUntil = rule.recur.until?.toTimestamp() if (tsUntil != null && tsUntil <= tsStart) { logger.warning("Ignoring $rule because UNTIL ($tsUntil) is not after DTSTART ($tsStart)") continue diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt index b680b4b2..61010f14 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt @@ -12,12 +12,12 @@ import at.bitfire.ical4android.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA import at.bitfire.ical4android.ICalendar import at.bitfire.ical4android.Task import at.bitfire.ical4android.UnknownProperty -import at.bitfire.ical4android.util.DateUtils.toEpochMilli import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.storage.BatchOperation.CpoBuilder import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.synctools.storage.tasks.TasksBatchOperation import at.bitfire.synctools.util.AndroidTimeUtils +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.TimeZone @@ -156,8 +156,8 @@ class DmfsTaskBuilder( .withValue(Tasks.CREATED, task.createdAt) .withValue(Tasks.LAST_MODIFIED, task.lastModified) - .withValue(Tasks.DTSTART, task.dtStart?.date?.toEpochMilli()) - .withValue(Tasks.DUE, task.due?.date?.toEpochMilli()) + .withValue(Tasks.DTSTART, task.dtStart?.date?.toTimestamp()) + .withValue(Tasks.DUE, task.due?.date?.toTimestamp()) .withValue(Tasks.DURATION, task.duration?.value) .withValue(Tasks.RDATE, 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 68af4eb8..68fef95b 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 @@ -10,7 +10,7 @@ import android.content.ContentValues import at.bitfire.ical4android.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA import at.bitfire.ical4android.Task import at.bitfire.ical4android.UnknownProperty -import at.bitfire.ical4android.util.DateUtils.toLocalDate +import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.synctools.util.AndroidTimeUtils 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 b6d34848..8e8069fa 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt @@ -6,8 +6,8 @@ package at.bitfire.synctools.util -import at.bitfire.ical4android.util.DateUtils.toLocalDate import at.bitfire.ical4android.util.TimeApiExtensions +import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import net.fortuna.ical4j.model.CalendarDateFormat import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.TemporalAdapter @@ -17,6 +17,7 @@ import net.fortuna.ical4j.model.parameter.TzId import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.DateListProperty import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.util.TimeZones import java.time.Duration import java.time.Instant import java.time.LocalDate @@ -27,6 +28,7 @@ import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.temporal.ChronoField +import java.time.temporal.Temporal import java.time.temporal.TemporalAmount import java.util.logging.Logger import kotlin.jvm.optionals.getOrDefault @@ -45,6 +47,67 @@ object AndroidTimeUtils { get() = Logger.getLogger(javaClass.name) + /** + * Converts this [Temporal] to the timestamp that should be used when writing an event to the + * Android calendar provider or task providers. + */ + fun Temporal.toTimestamp(): Long { + val epochSeconds = when (this) { + is LocalDate -> atStartOfDay().atZone(TimeZones.getDateTimeZone().toZoneId()).toEpochSecond() + is LocalDateTime -> atZone(TimeZones.getDefault().toZoneId()).toEpochSecond() + is OffsetDateTime -> toEpochSecond() + is ZonedDateTime -> toEpochSecond() + is Instant -> epochSecond + else -> error("Unsupported Temporal type: ${this::class.qualifiedName}") + } + + return epochSeconds * 1000L + } + + /** + * Converts this [Temporal] to a [ZonedDateTime] that is created from the timestamp returned by + * [toTimestamp] and the time zone returned by [androidTimezoneId]. + */ + fun Temporal.toZonedDateTime(): ZonedDateTime { + return Instant.ofEpochMilli(toTimestamp()).atZone(ZoneId.of(androidTimezoneId())) + } + + /** + * Returns the timezone ID that should be used when writing an event to the Android calendar + * provider or task providers. + * + * Note: For date-times with a given time zone, it needs to be a system time zone. Call + * [at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate] on dates coming from + * ical4j before calling this function. + * + * @return - "UTC" for dates and UTC date-times + * - the specified time zone ID for date-times with given time zone + * - the currently set default time zone ID for floating date-times + */ + fun Temporal.androidTimezoneId(): String { + return if (TemporalAdapter.isDateTimePrecision(this)) { + if (TemporalAdapter.isUtc(this)) { + TZID_UTC + } else if (TemporalAdapter.isFloating(this)) { + ZoneId.systemDefault().id + } else { + require(this is ZonedDateTime) { "Non-floating date-time must be a ZonedDateTime" } + + val timezoneId = this.zone.id + require(!timezoneId.startsWith("ical4j")) { + "ical4j ZoneIds are not supported. Call DatePropertyTzMapper.normalizedDate() " + + "before passing a date to this function." + } + + timezoneId + } + } else { + // For all-day events EventsColumns.EVENT_TIMEZONE must be "UTC". + TZID_UTC + } + } + + // recurrence sets /** diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapperTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapperTest.kt deleted file mode 100644 index 29f1c488..00000000 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapperTest.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.calendar.builder - -import at.bitfire.DefaultTimezoneRule -import at.bitfire.synctools.icalendar.requireDtStart -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.androidTimezoneId -import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp -import net.fortuna.ical4j.data.CalendarBuilder -import net.fortuna.ical4j.model.Component -import net.fortuna.ical4j.model.component.VEvent -import org.junit.Assert.assertEquals -import org.junit.Assert.fail -import org.junit.Rule -import org.junit.Test -import java.io.StringReader -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.chrono.JapaneseDate -import java.time.temporal.Temporal - -class AndroidTemporalMapperTest { - - @get:Rule - val tzRule = DefaultTimezoneRule("Europe/Vienna") - - @Test - fun `toTimestamp on LocalDate should use start of UTC day`() { - val date = LocalDate.of(2026, 3, 12) - - val timestamp = date.toTimestamp() - - assertEquals(1773273600000L, timestamp) - } - - @Test - fun `toTimestamp on LocalDateTime should use system default time zone`() { - val date = LocalDateTime.of(2026, 3, 12, 12, 34, 56) - - val timestamp = date.toTimestamp() - - assertEquals(1773315296000L, timestamp) - } - - @Test - fun `toTimestamp on OffsetDateTime`() { - val date = OffsetDateTime.of(2026, 3, 12, 12, 0, 0, 0, ZoneOffset.ofHours(3)) - - val timestamp = date.toTimestamp() - - assertEquals(1773306000000L, timestamp) - } - - @Test - fun `toTimestamp on ZonedDateTime`() { - val date = ZonedDateTime.of(2026, 3, 12, 12, 0, 0, 0, ZoneId.of("Europe/Helsinki")) - - val timestamp = date.toTimestamp() - - assertEquals(1773309600000L, timestamp) - } - - @Test - fun `toTimestamp on Instant`() { - val inputTimestamp = 1773273600000L - val date = Instant.ofEpochMilli(inputTimestamp) - - val timestamp = date.toTimestamp() - - assertEquals(inputTimestamp, timestamp) - } - - @Test - fun `toTimestamp on unsupported type`() { - try { - JapaneseDate.now().toTimestamp() - - fail("Expected exception") - } catch (e: IllegalStateException) { - assertEquals("Unsupported Temporal type: java.time.chrono.JapaneseDate", e.message) - } - } - - - @Test - fun `androidTimezoneId on LocalDate`() { - val date = LocalDate.now() - - val timezoneId = date.androidTimezoneId() - - assertEquals("UTC", timezoneId) - } - - @Test - fun `androidTimezoneId on LocalDateTime`() { - val date = LocalDateTime.now() - - val timezoneId = date.androidTimezoneId() - - assertEquals(tzRule.defaultZoneId.id, timezoneId) - } - - @Test - fun `androidTimezoneId on ZonedDateTime`() { - val date = LocalDateTime.now().atZone(ZoneId.of("Europe/Dublin")) - - val timezoneId = date.androidTimezoneId() - - assertEquals("Europe/Dublin", timezoneId) - } - - @Test - fun `androidTimezoneId on Instant`() { - val date = Instant.now() - - val timezoneId = date.androidTimezoneId() - - assertEquals("UTC", timezoneId) - } - - @Test - fun `androidTimezoneId on OffsetDateTime`() { - try { - OffsetDateTime.now().androidTimezoneId() - - fail("Expected exception") - } catch (e: IllegalArgumentException) { - assertEquals("Non-floating date-time must be a ZonedDateTime", e.message) - } - } - - @Test - fun `androidTimezoneId on ZonedDateTime from ical4j`() { - val cal = CalendarBuilder().build(StringReader( - """ - BEGIN:VCALENDAR - VERSION:2.0 - BEGIN:VTIMEZONE - TZID:Etc/ABC - BEGIN:STANDARD - TZNAME:-03 - TZOFFSETFROM:-0300 - TZOFFSETTO:-0300 - DTSTART:19700101T000000 - END:STANDARD - END:VTIMEZONE - BEGIN:VEVENT - SUMMARY:Test Timezones - DTSTART;TZID=Etc/ABC:20250828T130000 - END:VEVENT - END:VCALENDAR - """.trimIndent() - )) - val vEvent = cal.getComponent(Component.VEVENT).get() - val date = vEvent.requireDtStart().date - - try { - date.androidTimezoneId() - - fail("Expected exception") - } catch (e: IllegalArgumentException) { - assertEquals( - "ical4j ZoneIds are not supported. Call DatePropertyTzMapper.normalizedDate() " + - "before passing a date to this function.", - e.message - ) - } - } - -} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt index 989d703c..7d28e3a2 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt @@ -6,28 +6,47 @@ package at.bitfire.synctools.util +import at.bitfire.DefaultTimezoneRule import at.bitfire.dateTimeValue import at.bitfire.dateValue +import at.bitfire.synctools.icalendar.requireDtStart +import at.bitfire.synctools.util.AndroidTimeUtils.androidTimezoneId +import at.bitfire.synctools.util.AndroidTimeUtils.toTimestamp import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.parameter.TzId import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.ExDate import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.fail +import org.junit.Rule import org.junit.Test import java.io.StringReader import java.time.DateTimeException import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.chrono.JapaneseDate import java.time.format.DateTimeParseException +import java.time.temporal.Temporal import java.time.zone.ZoneRulesException import java.util.Optional class AndroidTimeUtilsTest { + @get:Rule + val tzRule = DefaultTimezoneRule("Europe/Vienna") + val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()!! val tzBerlin: TimeZone = tzRegistry.getTimeZone("Europe/Berlin")!! val tzToronto: TimeZone = tzRegistry.getTimeZone("America/Toronto")!! @@ -412,4 +431,147 @@ class AndroidTimeUtilsTest { } */ + @Test + fun `toTimestamp on LocalDate should use start of UTC day`() { + val date = LocalDate.of(2026, 3, 12) + + val timestamp = date.toTimestamp() + + assertEquals(1773273600000L, timestamp) + } + + @Test + fun `toTimestamp on LocalDateTime should use system default time zone`() { + val date = LocalDateTime.of(2026, 3, 12, 12, 34, 56) + + val timestamp = date.toTimestamp() + + assertEquals(1773315296000L, timestamp) + } + + @Test + fun `toTimestamp on OffsetDateTime`() { + val date = OffsetDateTime.of(2026, 3, 12, 12, 0, 0, 0, ZoneOffset.ofHours(3)) + + val timestamp = date.toTimestamp() + + assertEquals(1773306000000L, timestamp) + } + + @Test + fun `toTimestamp on ZonedDateTime`() { + val date = ZonedDateTime.of(2026, 3, 12, 12, 0, 0, 0, ZoneId.of("Europe/Helsinki")) + + val timestamp = date.toTimestamp() + + assertEquals(1773309600000L, timestamp) + } + + @Test + fun `toTimestamp on Instant`() { + val inputTimestamp = 1773273600000L + val date = Instant.ofEpochMilli(inputTimestamp) + + val timestamp = date.toTimestamp() + + assertEquals(inputTimestamp, timestamp) + } + + @Test + fun `toTimestamp on unsupported type`() { + try { + JapaneseDate.now().toTimestamp() + + fail("Expected exception") + } catch (e: IllegalStateException) { + assertEquals("Unsupported Temporal type: java.time.chrono.JapaneseDate", e.message) + } + } + + + @Test + fun `androidTimezoneId on LocalDate`() { + val date = LocalDate.now() + + val timezoneId = date.androidTimezoneId() + + assertEquals("UTC", timezoneId) + } + + @Test + fun `androidTimezoneId on LocalDateTime`() { + val date = LocalDateTime.now() + + val timezoneId = date.androidTimezoneId() + + assertEquals(tzRule.defaultZoneId.id, timezoneId) + } + + @Test + fun `androidTimezoneId on ZonedDateTime`() { + val date = LocalDateTime.now().atZone(ZoneId.of("Europe/Dublin")) + + val timezoneId = date.androidTimezoneId() + + assertEquals("Europe/Dublin", timezoneId) + } + + @Test + fun `androidTimezoneId on Instant`() { + val date = Instant.now() + + val timezoneId = date.androidTimezoneId() + + assertEquals("UTC", timezoneId) + } + + @Test + fun `androidTimezoneId on OffsetDateTime`() { + try { + OffsetDateTime.now().androidTimezoneId() + + fail("Expected exception") + } catch (e: IllegalArgumentException) { + assertEquals("Non-floating date-time must be a ZonedDateTime", e.message) + } + } + + @Test + fun `androidTimezoneId on ZonedDateTime from ical4j`() { + val cal = CalendarBuilder().build(StringReader( + """ + BEGIN:VCALENDAR + VERSION:2.0 + BEGIN:VTIMEZONE + TZID:Etc/ABC + BEGIN:STANDARD + TZNAME:-03 + TZOFFSETFROM:-0300 + TZOFFSETTO:-0300 + DTSTART:19700101T000000 + END:STANDARD + END:VTIMEZONE + BEGIN:VEVENT + SUMMARY:Test Timezones + DTSTART;TZID=Etc/ABC:20250828T130000 + END:VEVENT + END:VCALENDAR + """.trimIndent() + )) + val vEvent = cal.getComponent(Component.VEVENT).get() + val date = vEvent.requireDtStart().date + + try { + date.androidTimezoneId() + + fail("Expected exception") + } catch (e: IllegalArgumentException) { + assertEquals( + "ical4j ZoneIds are not supported. Call DatePropertyTzMapper.normalizedDate() " + + "before passing a date to this function.", + e.message + ) + } + } + } \ No newline at end of file