Skip to content

Commit

Permalink
Do not crash on RDATEs with PERIOD (#26)
Browse files Browse the repository at this point in the history
Should close bitfireAT/davx5#74

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
  • Loading branch information
sunkup and rfc2822 committed Apr 25, 2022
1 parent 89d7873 commit eb9261f
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 43 deletions.
61 changes: 37 additions & 24 deletions src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ object AndroidTimeUtils {
fun androidifyTimeZone(date: DateProperty?) {
if (DateUtils.isDateTime(date) && date?.isUtc == false) {
val tzID = date.timeZone?.id
val bestMatchingTzId = DateUtils.findAndroidTimezoneID(tzID)
if (tzID != bestMatchingTzId) {
Ical4Android.log.warning("Android doesn't know time zone ${tzID ?: "(floating)"}, setting default time zone $bestMatchingTzId")
date.timeZone = DateUtils.ical4jTimeZone(bestMatchingTzId)
}
date.timeZone = bestMatchingTzId(tzID)
}
}

Expand All @@ -73,18 +69,35 @@ object AndroidTimeUtils {
* @param dateList [DateListProperty] to validate. Values which are not DATE-TIME will be ignored.
*/
fun androidifyTimeZone(dateList: DateListProperty) {
// periods (RDate only)
val periods = (dateList as? RDate)?.periods
if (periods != null && periods.size > 0 && !periods.isUtc) {
val tzID = periods.timeZone?.id

// Won't work until resolved in ical4j (https://github.com/ical4j/ical4j/discussions/568)
// DateListProperty.setTimeZone() does not set the timeZone property when the DateList has PERIODs
dateList.timeZone = bestMatchingTzId(tzID)

return // RDate can only contain periods OR dates - not both, bail out fast
}

// date-times (RDate and ExDate)
val dates = dateList.dates
if (dates.type == Value.DATE_TIME && !dates.isUtc) {
val tzID = dateList.dates.timeZone?.id
val bestMatchingTzId = DateUtils.findAndroidTimezoneID(tzID)
if (tzID != bestMatchingTzId) {
Ical4Android.log.warning("Android doesn't know time zone ${tzID ?: "(floating)"}, setting default time zone $bestMatchingTzId")
dateList.timeZone = DateUtils.ical4jTimeZone(bestMatchingTzId)
if (dates != null && dates.size > 0) {
if (dates.type == Value.DATE_TIME && !dates.isUtc) {
val tzID = dates.timeZone?.id
dateList.timeZone = bestMatchingTzId(tzID)
}
}
}

// keep the time zone of dateList in sync with the actual dates
if (dateList.timeZone != dates.timeZone)
dateList.timeZone = dates.timeZone
private fun bestMatchingTzId(tzID: String?): TimeZone? {
val bestMatchingTzId = DateUtils.findAndroidTimezoneID(tzID)
return if (tzID == bestMatchingTzId) {
DateUtils.ical4jTimeZone(tzID)
} else {
Ical4Android.log.warning("Android doesn't know time zone ${tzID ?: "\"null\" (floating)"}, setting default time zone $bestMatchingTzId")
DateUtils.ical4jTimeZone(bestMatchingTzId)
}
}

Expand Down Expand Up @@ -122,6 +135,7 @@ object AndroidTimeUtils {
/**
* Concatenates, if necessary, multiple RDATE/EXDATE lists and converts them to
* a formatted string which Android calendar provider can process.
*
* Android expects this format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss" (when
* TZID is given) or "yyyymmddThhmmssZ". We don't use the TZID format here because then we're limited
* to one time-zone, while an iCalendar may contain multiple EXDATE/RDATE lines with different time zones.
Expand All @@ -140,15 +154,15 @@ object AndroidTimeUtils {
val strDates = LinkedList<String>()

// use time zone of first entry for the whole set; null for UTC
val tz = dates.firstOrNull()?.dates?.timeZone
val tz =
(dates.firstOrNull() as? RDate)?.periods?.timeZone ?: // VALUE=PERIOD (only RDate)
dates.firstOrNull()?.dates?.timeZone // VALUE=DATE/DATE-TIME

for (dateListProp in dates) {
if (dateListProp is RDate)
if (dateListProp.periods.isNotEmpty())
Ical4Android.log.warning("RDATE PERIOD not supported, ignoring")
else if (dateListProp is ExDate)
if (dateListProp.periods.isNotEmpty())
Ical4Android.log.warning("EXDATE PERIOD not supported, ignoring")
if (dateListProp is RDate && dateListProp.periods.isNotEmpty()) {
Ical4Android.log.warning("RDATE PERIOD not supported, ignoring")
break
}

when (dateListProp.dates.type) {
Value.DATE_TIME -> {
Expand All @@ -172,9 +186,8 @@ object AndroidTimeUtils {

// format: [tzid;]value1,value2,...
val result = StringBuilder()
if (tz != null) {
if (tz != null)
result.append(tz.id).append(RECURRENCE_LIST_TZID_SEPARATOR)
}
result.append(strDates.joinToString(RECURRENCE_LIST_VALUE_SEPARATOR))
return result.toString()
}
Expand Down Expand Up @@ -277,7 +290,7 @@ object AndroidTimeUtils {
}


// duration
// duration

/**
* Checks and fixes [Event.duration] values with incorrect format which can't be
Expand Down
137 changes: 118 additions & 19 deletions src/test/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import net.fortuna.ical4j.model.property.RDate
import net.fortuna.ical4j.util.TimeZones
import org.junit.Assert.*
import org.junit.Test
import java.io.InputStreamReader
import java.io.StringReader
import java.time.Duration
import java.time.Period
Expand All @@ -41,19 +42,23 @@ class AndroidTimeUtilsTest {
"END:STANDARD\n" +
"END:VTIMEZONE\n" +
"END:VCALENDAR"))
net.fortuna.ical4j.model.TimeZone(cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone)
TimeZone(cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone)
}

val tzIdDefault = java.util.TimeZone.getDefault().id
val tzDefault = DateUtils.ical4jTimeZone(tzIdDefault)

// androidifyTimeZone

@Test
fun testAndroidifyTimeZone_DateProperty_Null() {
fun testAndroidifyTimeZone_Null() {
// must not throw an exception
AndroidTimeUtils.androidifyTimeZone(null)
}

// androidifyTimeZone
// DateProperty

@Test
fun testAndroidifyTimeZone_DateProperty_Date() {
// dates (without time) should be ignored
Expand Down Expand Up @@ -104,6 +109,8 @@ class AndroidTimeUtilsTest {
assertTrue(dtStart.isUtc)
}

// androidifyTimeZone
// DateListProperty - date

@Test
fun testAndroidifyTimeZone_DateListProperty_Date() {
Expand All @@ -118,6 +125,9 @@ class AndroidTimeUtilsTest {
assertFalse(rDate.dates.isUtc)
}

// androidifyTimeZone
// DateListProperty - date-time

@Test
fun testAndroidifyTimeZone_DateListProperty_KnownTimeZone() {
// times with known time zone should be unchanged
Expand Down Expand Up @@ -170,6 +180,81 @@ class AndroidTimeUtilsTest {
assertTrue(rDate.dates.isUtc)
}

// androidifyTimeZone
// DateListProperty - period-explicit

@Test
fun testAndroidifyTimeZone_DateListProperty_Period_FloatingTime() {
// times with floating time should be treated as system default time zone
val rDate = RDate(PeriodList("19970101T180000/19970102T070000,20220103T000000/20220108T000000"))
AndroidTimeUtils.androidifyTimeZone(rDate)
assertEquals(
setOf(Period(DateTime("19970101T18000000"), DateTime("19970102T07000000")),
Period(DateTime("20220103T000000"), DateTime("20220108T000000"))),
rDate.periods)
assertNull(rDate.timeZone)
assertNull(rDate.periods.timeZone)
assertTrue(rDate.periods.isUtc)
}

@Test
fun testAndroidifyTimeZone_DateListProperty_Period_KnownTimezone() {
// periods with known time zone should be unchanged
val rDate = RDate(PeriodList("19970101T180000/19970102T070000,19970102T180000/19970108T090000"))
rDate.periods.timeZone = tzToronto
AndroidTimeUtils.androidifyTimeZone(rDate)
assertEquals(
setOf(Period("19970101T180000/19970102T070000"), Period("19970102T180000/19970108T090000")),
mutableSetOf<net.fortuna.ical4j.model.Period>().also { it.addAll(rDate.periods) }
)
assertEquals(tzToronto, rDate.periods.timeZone)
assertNull(rDate.timeZone)
assertFalse(rDate.dates.isUtc)
}

@Test
fun testAndroidifyTimeZone_DateListProperty_Periods_UnknownTimeZone() {
// time zone that is not available on Android systems should be rewritten to system default
val rDate = RDate(PeriodList("19970101T180000/19970102T070000,19970102T180000/19970108T090000"))
rDate.periods.timeZone = tzCustom
AndroidTimeUtils.androidifyTimeZone(rDate)
assertEquals(
setOf(Period("19970101T180000/19970102T070000"), Period("19970102T180000/19970108T090000")),
mutableSetOf<net.fortuna.ical4j.model.Period>().also { it.addAll(rDate.periods) }
)
assertEquals(tzIdDefault, rDate.periods.timeZone.id)
assertNull(rDate.timeZone)
assertFalse(rDate.dates.isUtc)
}

@Test
fun testAndroidifyTimeZone_DateListProperty_Period_UTC() {
// times with UTC should be unchanged
val rDate = RDate(PeriodList("19970101T180000Z/19970102T070000Z,20220103T0000Z/20220108T0000Z"))
AndroidTimeUtils.androidifyTimeZone(rDate)
assertEquals(
setOf(Period(DateTime("19970101T180000Z"), DateTime("19970102T070000Z")),
Period(DateTime("20220103T0000Z"), DateTime("20220108T0000Z"))),
rDate.periods)
assertTrue(rDate.periods.isUtc)
}

// androidifyTimeZone
// DateListProperty - period-start

@Test
fun testAndroidifyTimeZone_DateListProperty_PeriodStart_UTC() {
// times with UTC should be unchanged
val rDate = RDate(PeriodList("19970101T180000Z/PT5H30M,20220103T0000Z/PT2H30M10S"))
AndroidTimeUtils.androidifyTimeZone(rDate)
assertEquals(
setOf(Period(DateTime("19970101T180000Z"), Duration.parse("PT5H30M")),
Period(DateTime("20220103T0000Z"), Duration.parse("PT2H30M10S"))),
rDate.periods)
assertTrue(rDate.periods.isUtc)
}

// storageTzId

@Test
fun testStorageTzId_Date() =
Expand All @@ -189,14 +274,14 @@ class AndroidTimeUtilsTest {
}


// recurrence sets
// androidStringToRecurrenceSets

@Test
fun testAndroidStringToRecurrenceSets_UtcTimes() {
// list of UTC times
var exDate = AndroidTimeUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", false) { ExDate(it) }
val exDate = AndroidTimeUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", false) { ExDate(it) }
assertNull(exDate.timeZone)
var exDates = exDate.dates
val exDates = exDate.dates
assertEquals(Value.DATE_TIME, exDates.type)
assertTrue(exDates.isUtc)
assertEquals(2, exDates.size)
Expand Down Expand Up @@ -235,11 +320,31 @@ class AndroidTimeUtilsTest {
assertEquals(0, exDate.dates.size)
}

// recurrenceSetsToAndroidString

@Test
fun testRecurrenceSetsToAndroidString_UtcTime() {
fun testRecurrenceSetsToAndroidString_Date() {
// DATEs (without time) have to be converted to <date>T000000Z for Android
val list = ArrayList<DateListProperty>(1)
list.add(RDate(DateList("20150101T103010Z,20150102T103020Z", Value.DATE_TIME)))
assertEquals("20150101T103010Z,20150102T103020Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false))
list.add(RDate(DateList("20150101,20150702", Value.DATE)))
assertEquals("20150101T000000Z,20150702T000000Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, true))
}

@Test
fun testRecurrenceSetsToAndroidString_Period() {
// PERIODs are not supported yet — should be implemented later
val list = listOf(
RDate(PeriodList("19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))
)
assertEquals("", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false))
}

@Test
fun testRecurrenceSetsToAndroidString_TimeAlthoughAllDay() {
// DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to <date>T000000Z for Android
val list = ArrayList<DateListProperty>(1)
list.add(RDate(DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME)))
assertEquals("20150101T000000Z,20150702T000000Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, true))
}

@Test
Expand Down Expand Up @@ -274,20 +379,14 @@ class AndroidTimeUtilsTest {
}

@Test
fun testRecurrenceSetsToAndroidString_Date() {
// DATEs (without time) have to be converted to <date>T000000Z for Android
fun testRecurrenceSetsToAndroidString_UtcTime() {
val list = ArrayList<DateListProperty>(1)
list.add(RDate(DateList("20150101,20150702", Value.DATE)))
assertEquals("20150101T000000Z,20150702T000000Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, true))
list.add(RDate(DateList("20150101T103010Z,20150102T103020Z", Value.DATE_TIME)))
assertEquals("20150101T103010Z,20150102T103020Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, false))
}

@Test
fun testRecurrenceSetsToAndroidString_TimeAlthoughAllDay() {
// DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to <date>T000000Z for Android
val list = ArrayList<DateListProperty>(1)
list.add(RDate(DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME)))
assertEquals("20150101T000000Z,20150702T000000Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, true))
}

// recurrenceSetsToOpenTasksString

@Test
fun testRecurrenceSetsToOpenTasksString_UtcTimes() {
Expand Down

0 comments on commit eb9261f

Please sign in to comment.