Commit 605b9045 authored by Ricki Hirner's avatar Ricki Hirner 🐑
Browse files

Event: improved local handling of exceptions of recurring events

parent c1442429
Pipeline #106491270 passed with stages
in 5 minutes and 32 seconds
......@@ -254,9 +254,7 @@ abstract class AndroidEvent(
// exceptions from recurring events
row.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime ->
var originalAllDay = false
row.getAsInteger(Events.ORIGINAL_ALL_DAY)?.let { originalAllDay = it != 0 }
val originalAllDay = (row.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0
val originalDate = if (originalAllDay)
Date(originalInstanceTime) else
DateTime(originalInstanceTime)
......
......@@ -15,6 +15,7 @@ import net.fortuna.ical4j.model.TimeZone
import net.fortuna.ical4j.model.component.VTimeZone
import net.fortuna.ical4j.model.parameter.Value
import net.fortuna.ical4j.model.property.DateListProperty
import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.model.property.ExDate
import net.fortuna.ical4j.model.property.RDate
import java.io.StringReader
......@@ -74,6 +75,14 @@ object DateUtils {
return deviceTZ
}
/**
* Determines whether a given date represents a DATE-TIME value.
* @param date date property to check
* @return *true* if the date is a DATE-TIME value; *false* otherwise (for instance, when the
* date is a DATE value or null)
*/
fun isDateTime(date: DateProperty?) = date != null && date.date is DateTime
/**
* Parses a VTIMEZONE definition to a VTimeZone object.
* @param timezoneDef VTIMEZONE definition
......
......@@ -8,6 +8,7 @@
package at.bitfire.ical4android
import at.bitfire.ical4android.DateUtils.isDateTime
import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME
import net.fortuna.ical4j.data.CalendarOutputter
import net.fortuna.ical4j.data.ParserException
......@@ -206,24 +207,37 @@ class Event: ICalendar() {
// recurrence exceptions
for (exception in exceptions) {
// make sure that
// - exceptions have the same UID as the main event and
// - RECURRENCE-IDs have the same timezone as the main event's DTSTART
// exceptions must always have the same UID as the main event
exception.uid = uid
exception.recurrenceId?.let { recurrenceId ->
if (recurrenceId.timeZone != dtStart.timeZone) {
recurrenceId.timeZone = dtStart.timeZone
exception.recurrenceId = recurrenceId
}
// create VEVENT for exception
val vException = exception.toVEvent()
components += vException
val recurrenceId = exception.recurrenceId
if (recurrenceId == null) {
Constants.log.warning("Ignoring exception without recurrenceId")
continue
}
/* Exceptions must always have the same value type as DTSTART [RFC 5545 3.8.4.4].
If this is not the case, we don't add the exception to the event because we're
strict in what we send (and servers may reject such a case).
*/
if (isDateTime(recurrenceId) != isDateTime(dtStart)) {
Constants.log.warning("Ignoring exception $recurrenceId with other date type than dtStart: $dtStart")
continue
}
// remember used time zones
exception.dtStart?.timeZone?.let(usedTimeZones::add)
exception.dtEnd?.timeZone?.let(usedTimeZones::add)
// for simplicity and compatibility, rewrite date-time exceptions to the same time zone as DTSTART
if (isDateTime(recurrenceId) && recurrenceId.timeZone != dtStart.timeZone) {
Constants.log.fine("Changing timezone of $recurrenceId to same time zone as dtStart: $dtStart")
recurrenceId.timeZone = dtStart.timeZone
}
// create and add VEVENT for exception
val vException = exception.toVEvent()
components += vException
// remember used time zones
exception.dtStart?.timeZone?.let(usedTimeZones::add)
exception.dtEnd?.timeZone?.let(usedTimeZones::add)
}
// add VTIMEZONE components
......@@ -243,7 +257,7 @@ class Event: ICalendar() {
* @return generated VEvent
*/
private fun toVEvent(): VEvent {
val event = VEvent(true /* generates DTSTAMP */)
val event = VEvent(/* generates DTSTAMP */)
val props = event.properties
props += Uid(uid)
......@@ -258,7 +272,7 @@ class Event: ICalendar() {
description?.let { props += Description(it) }
color?.let { props += Color(null, it.name) }
props += dtStart
dtStart?.let { props += it }
dtEnd?.let { props += it }
duration?.let { props += it }
......
......@@ -99,8 +99,6 @@ open class ICalendar {
// time zone helpers
fun isDateTime(date: DateProperty?) = date != null && date.date is DateTime
/**
* Minifies a VTIMEZONE so that only components after [start] are kept.
* Doesn't return the smallest possible VTIMEZONE at the moment, but
......
......@@ -11,7 +11,7 @@ package at.bitfire.ical4android
import android.content.ContentValues
import android.database.Cursor
import android.database.DatabaseUtils
import net.fortuna.ical4j.model.TextList
import at.bitfire.ical4android.DateUtils.isDateTime
import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.util.TimeZones
import java.lang.reflect.Modifier
......@@ -25,7 +25,7 @@ object MiscUtils {
* @param date DateProperty to validate. Values which are not DATE-TIME will be ignored.
*/
fun androidifyTimeZone(date: DateProperty?) {
if (ICalendar.isDateTime(date)) {
if (isDateTime(date)) {
val tz = date!!.timeZone ?: return
val tzID = tz.id ?: return
val deviceTzID = DateUtils.findAndroidTimezoneID(tzID)
......@@ -47,7 +47,7 @@ object MiscUtils {
* - the currently set default time zone ID for floating date-times
*/
fun getTzId(date: DateProperty): String =
if (ICalendar.isDateTime(date)) {
if (isDateTime(date)) {
when {
date.isUtc ->
// DATE-TIME in UTC format
......
......@@ -7,10 +7,14 @@
*/
package at.bitfire.ical4android
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Dur
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.RRule
import net.fortuna.ical4j.model.property.RecurrenceId
import org.junit.Assert.*
import org.junit.Test
import java.io.ByteArrayOutputStream
......@@ -91,6 +95,44 @@ class EventTest {
assertEquals("Unknown Value", unknown.value)
}
@Test
fun testRecurringWriteFullDayException() {
val event = Event().apply {
uid = "test1"
dtStart = DtStart("20190117T083000", DateUtils.tzRegistry.getTimeZone("Europe/Berlin"))
summary = "Main event"
rRule = RRule("FREQ=DAILY;COUNT=5")
exceptions += arrayOf(
Event().apply {
uid = "test2"
recurrenceId = RecurrenceId(DateTime("20190118T073000", DateUtils.tzRegistry.getTimeZone("Europe/London")))
summary = "Normal exception"
},
Event().apply {
uid = "test3"
recurrenceId = RecurrenceId(Date("20190223"))
summary = "Full-day exception"
}
)
}
val baos = ByteArrayOutputStream()
event.write(baos)
val iCal = baos.toString()
assertTrue(iCal.contains("UID:test1\r\n"))
assertTrue(iCal.contains("DTSTART;TZID=Europe/Berlin:20190117T083000\r\n"))
// first RECURRENCE-ID has been rewritten
// - to main event's UID
// - to time zone Europe/Berlin (with one hour time difference)
assertTrue(iCal.contains("UID:test1\r\n" +
"RECURRENCE-ID;TZID=Europe/Berlin:20190118T083000\r\n" +
"SUMMARY:Normal exception\r\n" +
"END:VEVENT"))
// no RECURRENCE-ID;VALUE=DATE:20190223
assertFalse(iCal.contains(":20190223"))
}
@Test
fun testRecurringWithException() {
val event = parseCalendar("recurring-with-exception1.ics").first()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment