Skip to content

Commit 10e8cd9

Browse files
authored
Split TimeFieldsProcessor (#106)
* [WIP] StartTimeProcessor * [WIP] DurationProcessor * [WIP] Remove deprecated TimeFieldsProcessor and split into StartTimeProcessor, EndTimeProcessor, and DurationProcessor * Add DurationProcessorTest * Minor changes * Fix tests
1 parent 1fd1eaf commit 10e8cd9

21 files changed

+716
-361
lines changed

lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import android.content.ContentValues
1111
import android.net.Uri
1212
import android.os.RemoteException
1313
import androidx.annotation.CallSuper
14-
import at.bitfire.ical4android.util.AndroidTimeUtils
1514
import at.bitfire.synctools.storage.BatchOperation.CpoBuilder
1615
import at.bitfire.synctools.storage.LocalStorageException
1716
import at.bitfire.synctools.storage.TasksBatchOperation
1817
import at.bitfire.synctools.storage.toContentValues
18+
import at.bitfire.synctools.util.AndroidTimeUtils
1919
import net.fortuna.ical4j.model.Date
2020
import net.fortuna.ical4j.model.DateTime
2121
import net.fortuna.ical4j.model.Parameter

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessor.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,24 @@ import at.bitfire.synctools.mapping.calendar.processor.AvailabilityProcessor
1616
import at.bitfire.synctools.mapping.calendar.processor.CategoriesProcessor
1717
import at.bitfire.synctools.mapping.calendar.processor.ColorProcessor
1818
import at.bitfire.synctools.mapping.calendar.processor.DescriptionProcessor
19+
import at.bitfire.synctools.mapping.calendar.processor.DurationProcessor
20+
import at.bitfire.synctools.mapping.calendar.processor.EndTimeProcessor
1921
import at.bitfire.synctools.mapping.calendar.processor.LocationProcessor
2022
import at.bitfire.synctools.mapping.calendar.processor.MutatorsProcessor
2123
import at.bitfire.synctools.mapping.calendar.processor.OrganizerProcessor
2224
import at.bitfire.synctools.mapping.calendar.processor.OriginalInstanceTimeProcessor
2325
import at.bitfire.synctools.mapping.calendar.processor.RecurrenceFieldsProcessor
2426
import at.bitfire.synctools.mapping.calendar.processor.RemindersProcessor
2527
import at.bitfire.synctools.mapping.calendar.processor.SequenceProcessor
28+
import at.bitfire.synctools.mapping.calendar.processor.StartTimeProcessor
2629
import at.bitfire.synctools.mapping.calendar.processor.StatusProcessor
27-
import at.bitfire.synctools.mapping.calendar.processor.TimeFieldsProcessor
2830
import at.bitfire.synctools.mapping.calendar.processor.TitleProcessor
2931
import at.bitfire.synctools.mapping.calendar.processor.UidProcessor
3032
import at.bitfire.synctools.mapping.calendar.processor.UnknownPropertiesProcessor
3133
import at.bitfire.synctools.mapping.calendar.processor.UrlProcessor
3234
import at.bitfire.synctools.storage.calendar.EventAndExceptions
3335
import net.fortuna.ical4j.model.DateList
36+
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
3437
import net.fortuna.ical4j.model.parameter.Value
3538
import net.fortuna.ical4j.model.property.ExDate
3639
import net.fortuna.ical4j.model.property.RecurrenceId
@@ -48,15 +51,19 @@ class LegacyAndroidEventProcessor(
4851
private val accountName: String
4952
) {
5053

54+
private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()
55+
5156
private val fieldProcessors: Array<AndroidEventFieldProcessor> = arrayOf(
5257
// event row fields
5358
MutatorsProcessor(), // for PRODID
5459
UidProcessor(),
55-
OriginalInstanceTimeProcessor(),
60+
OriginalInstanceTimeProcessor(tzRegistry),
5661
TitleProcessor(),
5762
LocationProcessor(),
58-
TimeFieldsProcessor(),
59-
RecurrenceFieldsProcessor(),
63+
StartTimeProcessor(tzRegistry),
64+
EndTimeProcessor(tzRegistry),
65+
DurationProcessor(tzRegistry),
66+
RecurrenceFieldsProcessor(tzRegistry),
6067
DescriptionProcessor(),
6168
ColorProcessor(),
6269
AccessLevelProcessor(),

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import android.content.Entity
1010
import android.provider.CalendarContract.Events
1111
import androidx.annotation.VisibleForTesting
1212
import at.bitfire.ical4android.Event
13-
import at.bitfire.ical4android.util.AndroidTimeUtils
1413
import at.bitfire.ical4android.util.DateUtils
1514
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate
1615
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime
1716
import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate
1817
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
18+
import at.bitfire.synctools.util.AndroidTimeUtils
1919
import net.fortuna.ical4j.model.DateTime
2020
import net.fortuna.ical4j.model.property.DtEnd
2121
import net.fortuna.ical4j.model.property.DtStart

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ package at.bitfire.synctools.mapping.calendar.builder
99
import android.content.Entity
1010
import android.provider.CalendarContract.Events
1111
import at.bitfire.ical4android.Event
12-
import at.bitfire.ical4android.util.AndroidTimeUtils
12+
import at.bitfire.synctools.util.AndroidTimeUtils
1313
import net.fortuna.ical4j.model.DateList
1414
import net.fortuna.ical4j.model.property.RDate
1515
import java.util.logging.Logger

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ package at.bitfire.synctools.mapping.calendar.builder
99
import android.content.Entity
1010
import android.provider.CalendarContract.Events
1111
import at.bitfire.ical4android.Event
12-
import at.bitfire.ical4android.util.AndroidTimeUtils
1312
import at.bitfire.ical4android.util.DateUtils
13+
import at.bitfire.synctools.util.AndroidTimeUtils
1414
import java.time.ZoneId
1515

1616
class StartTimeBuilder: AndroidEntityBuilder {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.calendar.processor
8+
9+
import at.bitfire.synctools.util.AndroidTimeUtils
10+
import net.fortuna.ical4j.model.Date
11+
import net.fortuna.ical4j.model.DateTime
12+
import net.fortuna.ical4j.model.TimeZoneRegistry
13+
import net.fortuna.ical4j.util.TimeZones
14+
import java.time.ZoneId
15+
16+
/**
17+
* Converts timestamps from the [android.provider.CalendarContract.Events.DTSTART] or [android.provider.CalendarContract.Events.DTEND]
18+
* fields into other representations.
19+
*
20+
* @param timestamp value of the DTSTART/DTEND field (timestamp in milliseconds)
21+
* @param timeZone value of the respective timezone field ([android.provider.CalendarContract.Events.EVENT_TIMEZONE] / [android.provider.CalendarContract.Events.EVENT_END_TIMEZONE])
22+
* @param allDay whether [android.provider.CalendarContract.Events.ALL_DAY] is non-null and not zero
23+
*/
24+
class AndroidTimeField(
25+
private val timestamp: Long,
26+
private val timeZone: String?,
27+
private val allDay: Boolean,
28+
private val tzRegistry: TimeZoneRegistry
29+
) {
30+
31+
/** ID of system default timezone */
32+
private val defaultTzId by lazy { ZoneId.systemDefault().id }
33+
34+
/**
35+
* Converts the given Android date/time into an ical4j date property.
36+
*
37+
* @return `Date` in case of an all-day event, `DateTime` in case of a non-all-day event
38+
*/
39+
fun asIcal4jDate(): Date {
40+
if (allDay)
41+
return Date(timestamp)
42+
43+
// non-all-day
44+
val tzId = timeZone
45+
?: defaultTzId // safe fallback (should never be used because the calendar provider requires EVENT_TIMEZONE)
46+
47+
/* The resolved timezone may be null if there is no ical4j timezone for tzId, which can happen in rare cases
48+
(for instance if Android already knows about a new timezone ID or alias that doesn't exist in our
49+
ical4j version yet).
50+
51+
In this case, we use the system default timezone ID as fallback and hope that we have a VTIMEZONE for it.
52+
If we also don't have a VTIMEZONE for the default timezone, we fall back to a UTC DATE-TIME without timezone. */
53+
54+
val timezone = if (tzId == AndroidTimeUtils.TZID_UTC || tzId == TimeZones.UTC_ID || tzId == TimeZones.IBM_UTC_ID)
55+
null // indicates UTC
56+
else
57+
(tzRegistry.getTimeZone(tzId) ?: tzRegistry.getTimeZone(defaultTzId))
58+
59+
return DateTime(timestamp).also { dateTime ->
60+
if (timezone == null)
61+
dateTime.isUtc = true
62+
else
63+
dateTime.timeZone = timezone
64+
}
65+
}
66+
67+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.calendar.processor
8+
9+
import android.content.Entity
10+
import android.provider.CalendarContract.Events
11+
import at.bitfire.ical4android.Event
12+
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate
13+
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime
14+
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
15+
import at.bitfire.synctools.util.AndroidTimeUtils
16+
import net.fortuna.ical4j.model.DateTime
17+
import net.fortuna.ical4j.model.TimeZoneRegistry
18+
import net.fortuna.ical4j.model.property.DtEnd
19+
import java.time.Duration
20+
import java.time.Instant
21+
import java.time.Period
22+
import java.time.ZoneOffset
23+
24+
class DurationProcessor(
25+
private val tzRegistry: TimeZoneRegistry
26+
): AndroidEventFieldProcessor {
27+
28+
override fun process(from: Entity, main: Entity, to: Event) {
29+
val values = from.entityValues
30+
31+
/* Skip if:
32+
- DTEND is set – we don't need to process DURATION anymore.
33+
- DURATION is not set – then usually DTEND is set; however it's also OK to have neither DTEND nor DURATION in a VEVENT. */
34+
if (values.getAsLong(Events.DTEND) != null)
35+
return
36+
val durStr = values.getAsString(Events.DURATION) ?: return
37+
val duration = AndroidTimeUtils.parseDuration(durStr)
38+
39+
// Skip in case of zero or negative duration (analogous to DTEND being before DTSTART).
40+
if ((duration is Duration && (duration.isZero || duration.isNegative)) ||
41+
(duration is Period && (duration.isZero || duration.isNegative)))
42+
return
43+
44+
/* Some servers have problems with DURATION. For maximum compatibility, we always generate DTEND instead of DURATION.
45+
(After all, the constraint that non-recurring events have a DTEND while recurring events use DURATION is Android-specific.)
46+
So we have to calculate DTEND from DTSTART and its timezone plus DURATION. */
47+
48+
val tsStart = values.getAsLong(Events.DTSTART) ?: return
49+
val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0
50+
51+
if (allDay) {
52+
val startTimeUTC = Instant.ofEpochMilli(tsStart).atOffset(ZoneOffset.UTC)
53+
val endDate = (startTimeUTC + duration).toLocalDate()
54+
55+
// DATE
56+
to.dtEnd = DtEnd(endDate.toIcal4jDate())
57+
58+
} else {
59+
// DATE-TIME
60+
val startDateTime = AndroidTimeField(
61+
timestamp = tsStart,
62+
timeZone = values.getAsString(Events.EVENT_TIMEZONE),
63+
allDay = false,
64+
tzRegistry = tzRegistry
65+
).asIcal4jDate() as DateTime
66+
67+
val start = startDateTime.toZonedDateTime()
68+
val end = start + duration
69+
70+
to.dtEnd = DtEnd(end.toIcal4jDateTime())
71+
}
72+
}
73+
74+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.calendar.processor
8+
9+
import android.content.Entity
10+
import android.provider.CalendarContract.Events
11+
import at.bitfire.ical4android.Event
12+
import net.fortuna.ical4j.model.TimeZoneRegistry
13+
import net.fortuna.ical4j.model.property.DtEnd
14+
import java.util.logging.Logger
15+
16+
class EndTimeProcessor(
17+
private val tzRegistry: TimeZoneRegistry
18+
): AndroidEventFieldProcessor {
19+
20+
private val logger
21+
get() = Logger.getLogger(javaClass.name)
22+
23+
override fun process(from: Entity, main: Entity, to: Event) {
24+
val values = from.entityValues
25+
val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0
26+
27+
// Skip if DTEND is not set – then usually DURATION is set; however it's also OK to have neither DTEND nor DURATION in a VEVENT.
28+
val tsEnd = values.getAsLong(Events.DTEND) ?: return
29+
30+
// Also skip if DTEND is not after DTSTART (not allowed in iCalendar)
31+
val tsStart = values.getAsLong(Events.DTSTART) ?: return
32+
if (tsEnd <= tsStart) {
33+
logger.warning("Ignoring DTEND=$tsEnd that is not after DTSTART=$tsStart")
34+
return
35+
}
36+
37+
// DATE or DATE-TIME according to allDay
38+
val end = AndroidTimeField(
39+
timestamp = tsEnd,
40+
timeZone = values.getAsString(Events.EVENT_END_TIMEZONE)
41+
?: values.getAsString(Events.EVENT_TIMEZONE), // if end timezone is not present, assume same as for start
42+
allDay = allDay,
43+
tzRegistry = tzRegistry
44+
).asIcal4jDate()
45+
46+
to.dtEnd = DtEnd(end)
47+
}
48+
49+
}

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/OriginalInstanceTimeProcessor.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import at.bitfire.ical4android.Event
1212
import at.bitfire.ical4android.util.DateUtils
1313
import net.fortuna.ical4j.model.Date
1414
import net.fortuna.ical4j.model.DateTime
15-
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
15+
import net.fortuna.ical4j.model.TimeZoneRegistry
1616
import net.fortuna.ical4j.model.property.RecurrenceId
1717
import net.fortuna.ical4j.util.TimeZones
1818

19-
class OriginalInstanceTimeProcessor: AndroidEventFieldProcessor {
20-
21-
private val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() }
19+
class OriginalInstanceTimeProcessor(
20+
private val tzRegistry: TimeZoneRegistry
21+
): AndroidEventFieldProcessor {
2222

2323
override fun process(from: Entity, main: Entity, to: Event) {
2424
// only applicable to exceptions, not to main events

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/RecurrenceFieldsProcessor.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ package at.bitfire.synctools.mapping.calendar.processor
99
import android.content.Entity
1010
import android.provider.CalendarContract.Events
1111
import at.bitfire.ical4android.Event
12-
import at.bitfire.ical4android.util.AndroidTimeUtils
1312
import at.bitfire.synctools.exception.InvalidLocalResourceException
14-
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
13+
import at.bitfire.synctools.util.AndroidTimeUtils
14+
import net.fortuna.ical4j.model.TimeZoneRegistry
1515
import net.fortuna.ical4j.model.property.ExDate
1616
import net.fortuna.ical4j.model.property.ExRule
1717
import net.fortuna.ical4j.model.property.RDate
@@ -20,13 +20,13 @@ import java.util.LinkedList
2020
import java.util.logging.Level
2121
import java.util.logging.Logger
2222

23-
class RecurrenceFieldsProcessor: AndroidEventFieldProcessor {
23+
class RecurrenceFieldsProcessor(
24+
private val tzRegistry: TimeZoneRegistry
25+
): AndroidEventFieldProcessor {
2426

2527
private val logger
2628
get() = Logger.getLogger(javaClass.name)
2729

28-
private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()
29-
3030
override fun process(from: Entity, main: Entity, to: Event) {
3131
val values = from.entityValues
3232

0 commit comments

Comments
 (0)