diff options
author | Ricki Hirner <hirner@bitfire.at> | 2022-09-08 22:56:19 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-08 22:56:19 +0300 |
commit | 2c31b0e8860efaa7ba5ec53b244b48463b9cd694 (patch) | |
tree | 3a559a15afde3ef83f336424b54694d47f952a2f | |
parent | 5a889101b1c88d359e4817f1b3dcfad7d7824fd5 (diff) |
EventValidator: RRULE repairing inserts unwanted COUNT=-1 (#53)57-exception-when-unknown-timezone-in-calendar-provider
* EventValidator.sameTypeForDtStartAndRruleUntil: fix COUNT=-1 problem, add tests
* Tests
* EventValidator: always set UNTIL to UTC
* Move util classes; add TimeZone.toZoneIdCompat() tests
* Optimize imports
33 files changed, 191 insertions, 96 deletions
diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt index a8ab2f4..dbd9c4c 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidCalendarTest.kt @@ -14,10 +14,10 @@ import android.provider.CalendarContract.Calendars import android.provider.CalendarContract.Colors import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat -import at.bitfire.ical4android.MiscUtils.UriHelper.asSyncAdapter import at.bitfire.ical4android.impl.TestCalendar import at.bitfire.ical4android.impl.TestEvent +import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import org.junit.* diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt index c9ff0b2..556fa0b 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt @@ -13,11 +13,12 @@ import android.net.Uri import android.provider.CalendarContract.* import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat -import at.bitfire.ical4android.MiscUtils.UriHelper.asSyncAdapter import at.bitfire.ical4android.impl.TestCalendar import at.bitfire.ical4android.impl.TestEvent import at.bitfire.ical4android.util.AndroidTimeUtils +import at.bitfire.ical4android.util.DateUtils +import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.* diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt index c907439..61565cb 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTaskTest.kt @@ -12,6 +12,7 @@ import android.net.Uri import androidx.test.filters.MediumTest import at.bitfire.ical4android.impl.TestTask import at.bitfire.ical4android.impl.TestTaskList +import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt index d51c1ad..d6199ad 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidTimeZonesTest.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.util.DateUtils import org.junit.Assert import org.junit.Assert.assertNotNull import org.junit.Test diff --git a/src/androidTest/java/at/bitfire/ical4android/AospTest.kt b/src/androidTest/java/at/bitfire/ical4android/AospTest.kt index bf18803..c048f25 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AospTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AospTest.kt @@ -12,7 +12,7 @@ import android.net.Uri import android.provider.CalendarContract import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import org.junit.After import org.junit.Assert.assertNotNull import org.junit.Before diff --git a/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt b/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt index 66310df..4ef3148 100644 --- a/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt @@ -13,9 +13,9 @@ import android.provider.CalendarContract import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat import at.bitfire.ical4android.impl.TestCalendar import at.bitfire.ical4android.impl.TestEvent +import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart diff --git a/src/androidTest/java/at/bitfire/ical4android/EventTest.kt b/src/androidTest/java/at/bitfire/ical4android/EventTest.kt index f114bf3..e016b07 100644 --- a/src/androidTest/java/at/bitfire/ical4android/EventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/EventTest.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Parameter diff --git a/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt b/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt index ee0b944..a62fb72 100644 --- a/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.Date diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt b/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt index 29b5157..3e9f886 100644 --- a/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/JtxCollectionTest.kt @@ -9,8 +9,8 @@ import android.content.ContentProviderClient import android.content.ContentValues import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat import at.bitfire.ical4android.impl.TestJtxCollection +import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter import junit.framework.TestCase.* diff --git a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt index f82c07d..e0cb10a 100644 --- a/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -11,8 +11,8 @@ import android.database.DatabaseUtils import android.os.ParcelFileDescriptor import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat import at.bitfire.ical4android.impl.TestJtxCollection +import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.JtxICalObject import at.techbee.jtx.JtxContract.JtxICalObject.Component diff --git a/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt b/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt index cbe0bcc..f9f72af 100644 --- a/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.RelType diff --git a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt index 5a69060..2bef306 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxCollection.kt @@ -9,7 +9,7 @@ import android.content.ContentProviderClient import at.bitfire.ical4android.JtxCollection import at.bitfire.ical4android.JtxCollectionFactory import at.bitfire.ical4android.JtxICalObject -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues import at.techbee.jtx.JtxContract import java.util.* diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt index 9cb7653..18a37f9 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt @@ -2,9 +2,8 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.ical4android +package at.bitfire.ical4android.util -import at.bitfire.ical4android.util.AndroidTimeUtils import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.component.VTimeZone diff --git a/src/androidTest/java/at/bitfire/ical4android/DateUtilsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt index 395b8da..fe1f468 100644 --- a/src/androidTest/java/at/bitfire/ical4android/DateUtilsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.ical4android +package at.bitfire.ical4android.util import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime diff --git a/src/androidTest/java/at/bitfire/ical4android/MiscUtilsAndroidTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/MiscUtilsTest.kt index d6f7e40..dee8dd9 100644 --- a/src/androidTest/java/at/bitfire/ical4android/MiscUtilsAndroidTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/MiscUtilsTest.kt @@ -2,20 +2,20 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.ical4android +package at.bitfire.ical4android.util import android.accounts.Account import android.content.ContentValues import android.database.MatrixCursor import android.net.Uri import androidx.test.filters.SmallTest -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Test -class MiscUtilsAndroidTest { +class MiscUtilsTest { private val tzVienna = DateUtils.ical4jTimeZone("Europe/Vienna") diff --git a/src/androidTest/java/at/bitfire/ical4android/TimeApiExtensionsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt index 44a02a0..e5c6fc0 100644 --- a/src/androidTest/java/at/bitfire/ical4android/TimeApiExtensionsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.ical4android +package at.bitfire.ical4android.util import at.bitfire.ical4android.util.TimeApiExtensions.requireTimeZone import at.bitfire.ical4android.util.TimeApiExtensions.toDuration @@ -28,7 +28,18 @@ class TimeApiExtensionsTest { @Test - fun testDateToLocalDate() { + fun testTimeZone_toZoneIdCompat_NotUtc() { + assertEquals(ZoneId.of("Europe/Berlin"), tzBerlin.toZoneId()) + } + + @Test + fun testTimeZone_toZoneIdCompat_Utc() { + assertEquals(ZoneOffset.UTC, TimeZones.getUtcTimeZone().toZoneIdCompat()) + } + + + @Test + fun testDate_toLocalDate() { val date = Date("20200620").toLocalDate() assertEquals(2020, date.year) assertEquals(6, date.monthValue) @@ -38,19 +49,19 @@ class TimeApiExtensionsTest { @Test - fun testDateTimeRequireTimeZone() { + fun testDateTime_requireTimeZone() { val time = DateTime("2020707T010203", tzBerlin) assertEquals(tzBerlin, time.requireTimeZone()) } @Test - fun testDateTimeRequireTimeZone_Floating() { + fun testDateTime_requireTimeZone_Floating() { val time = DateTime("2020707T010203") assertEquals(TimeZone.getDefault(), time.requireTimeZone()) } @Test - fun testDateTimeRequireTimeZone_Utc() { + fun testDateTime_requireTimeZone_Utc() { val time = DateTime("2020707T010203Z").apply { isUtc = true } assertTrue(time.isUtc) assertEquals(TimeZones.getUtcTimeZone(), time.requireTimeZone()) @@ -58,7 +69,7 @@ class TimeApiExtensionsTest { @Test - fun testDateTimeToLocalDate_TimezoneBoundary() { + fun testDateTime_toLocalDate_TimezoneBoundary() { val date = DateTime("20200620T000000", tzBerlin).toLocalDate() assertEquals(2020, date.year) assertEquals(6, date.monthValue) @@ -67,7 +78,7 @@ class TimeApiExtensionsTest { } @Test - fun testDateTimeToLocalDate_TimezoneDuringDay() { + fun testDateTime_toLocalDate_TimezoneDuringDay() { val date = DateTime("20200620T123000", tzBerlin).toLocalDate() assertEquals(2020, date.year) assertEquals(6, date.monthValue) @@ -76,7 +87,7 @@ class TimeApiExtensionsTest { } @Test - fun testDateTimeToLocalDate_UtcDuringDay() { + fun testDateTime_toLocalDate_UtcDuringDay() { val date = DateTime("20200620T123000Z").apply { isUtc = true }.toLocalDate() assertEquals(2020, date.year) assertEquals(6, date.monthValue) @@ -86,23 +97,23 @@ class TimeApiExtensionsTest { @Test - fun testDateTimeToLocalTime() { + fun testDateTime_toLocalTime() { assertEquals(LocalTime.of(12, 30), DateTime("20200620T123000", tzBerlin).toLocalTime()) } @Test - fun testDateTimeToLocalTime_Floating() { + fun testDateTime_toLocalTime_Floating() { assertEquals(LocalTime.of(12, 30), DateTime("20200620T123000").toLocalTime()) } @Test - fun testDateTimeToLocalTime_Utc() { + fun testDateTime_toLocalTime_Utc() { assertEquals(LocalTime.of(12, 30), DateTime("20200620T123000Z").apply { isUtc = true }.toLocalTime()) } @Test - fun testDateTimeToZonedDateTime() { + fun testDateTime_toZonedDateTime() { assertEquals( ZonedDateTime.of(2020, 7, 7, 10, 30, 0, 0, tzBerlin.toZoneIdCompat()), DateTime("20200707T103000", tzBerlin).toZonedDateTime() @@ -110,7 +121,7 @@ class TimeApiExtensionsTest { } @Test - fun testDateTimeToZonedDateTime_Floating() { + fun testDateTime_toZonedDateTime_Floating() { assertEquals( ZonedDateTime.of(2020, 7, 7, 10, 30, 0, 0, ZoneId.systemDefault()), DateTime("20200707T103000").toZonedDateTime() @@ -118,7 +129,7 @@ class TimeApiExtensionsTest { } @Test - fun testDateTimeToZonedDateTime_UTC() { + fun testDateTime_toZonedDateTime_UTC() { assertEquals( ZonedDateTime.of(2020, 7, 7, 10, 30, 0, 0, ZoneOffset.UTC), DateTime("20200707T103000Z").apply { isUtc = true }.toZonedDateTime() @@ -127,31 +138,31 @@ class TimeApiExtensionsTest { @Test - fun testLocalDateToIcal4jDate() { + fun testLocalDate_toIcal4jDate() { assertEquals(Date("19000118"), LocalDate.of(1900, 1, 18).toIcal4jDate()) assertEquals(Date("20200620"), LocalDate.of(2020, 6, 20).toIcal4jDate()) } @Test - fun testZonedDateTimeToIcal4jDateTime() { + fun testZonedDateTime_toIcal4jDateTime_NotUtc() { val tzBerlin = DateUtils.ical4jTimeZone("Europe/Berlin") assertEquals( - DateTime("20200705T010203", tzBerlin), - ZonedDateTime.of(2020, 7, 5, 1, 2, 3, 0, ZoneId.of("Europe/Berlin")).toIcal4jDateTime() + DateTime("20200705T010203", tzBerlin), + ZonedDateTime.of(2020, 7, 5, 1, 2, 3, 0, ZoneId.of("Europe/Berlin")).toIcal4jDateTime() ) } @Test - fun testZonedDateTimeToIcal4jDateTime_Utc() { + fun testZonedDateTime_toIcal4jDateTime_Utc() { assertEquals( - DateTime("20200705T010203Z"), - ZonedDateTime.of(2020, 7, 5, 1, 2, 3, 0, ZoneOffset.UTC).toIcal4jDateTime() + DateTime("20200705T010203Z"), + ZonedDateTime.of(2020, 7, 5, 1, 2, 3, 0, ZoneOffset.UTC).toIcal4jDateTime() ) } @Test - fun testTemporalAmountToDuration() { + fun testTemporalAmount_toDuration() { assertEquals(Duration.ofHours(1), Duration.ofHours(1).toDuration(Instant.EPOCH)) assertEquals(Duration.ofDays(1), Duration.ofDays(1).toDuration(Instant.EPOCH)) assertEquals(Duration.ofDays(1), Period.ofDays(1).toDuration(Instant.EPOCH)) @@ -161,7 +172,7 @@ class TimeApiExtensionsTest { } @Test - fun testTemporalAmountToRfc5545Duration_Duration() { + fun testTemporalAmount_toRfc5545Duration_Duration() { assertEquals("P0S", Duration.ofDays(0).toRfc5545Duration(Instant.EPOCH)) assertEquals("P2W", Duration.ofDays(14).toRfc5545Duration(Instant.EPOCH)) assertEquals("P15D", Duration.ofDays(15).toRfc5545Duration(Instant.EPOCH)) diff --git a/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt b/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt index 2af3fe8..fa3d959 100644 --- a/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -6,14 +6,12 @@ package at.bitfire.ical4android.validation import at.bitfire.ical4android.Event import at.bitfire.ical4android.InvalidCalendarException -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Recur -import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule import org.junit.Assert.* +import org.junit.Assume import org.junit.Test import java.io.StringReader @@ -79,10 +77,10 @@ class EventValidatorTest { rRules.add(RRule("FREQ=MONTHLY;UNTIL=20251214T001100Z")) // DATETIME (UTC) } assertEquals(DateTime("20211115T001100Z"), event.dtStart!!.date) - assertEquals(DateTime("20251214T001100Z"), event.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20251214T001100Z", event.rRules.joinToString()) EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) assertEquals(DateTime("20211115T001100Z"), event.dtStart!!.date) - assertEquals(DateTime("20251214T001100Z"), event.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20251214T001100Z", event.rRules.joinToString()) val event1 = Event.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + @@ -92,7 +90,7 @@ class EventValidatorTest { "RRULE:FREQ=MONTHLY;UNTIL=20231214;BYMONTHDAY=15\n" + // DATE "END:VEVENT\n" + "END:VCALENDAR")).first() - assertEquals(Date("20231214"), event1.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20231214;BYMONTHDAY=15", event1.rRules.joinToString()) val event2 = Event.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + @@ -102,7 +100,7 @@ class EventValidatorTest { "RRULE:FREQ=YEARLY;UNTIL=20230216;BYMONTHDAY=15\n" + // DATE "END:VEVENT\n" + "END:VCALENDAR")).first() - assertEquals(Date("20230216"), event2.rRules.first.recur.until) + assertEquals("FREQ=YEARLY;UNTIL=20230216;BYMONTHDAY=15", event2.rRules.joinToString()) } @Test @@ -119,7 +117,7 @@ class EventValidatorTest { event.rRules.first.recur.until ) EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) - assertEquals(Date("20211214"), event.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20211214", event.rRules.joinToString()) val event1 = Event.eventsFromReader( StringReader( @@ -133,7 +131,7 @@ class EventValidatorTest { ) ).first() assertEquals(1639440000000, event1.rRules.first.recur.until.time) - assertEquals(Date("20211214"), event1.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20211214;BYMONTHDAY=15", event1.rRules.joinToString()) } @Test @@ -150,22 +148,22 @@ class EventValidatorTest { // As it does not happen often, for the sake of simplicity we just accept either EventValidator.sameTypeForDtStartAndRruleUntil(event2.dtStart!!, event2.rRules) assertTrue( - Date("20230218") == event2.rRules.first.recur.until || - Date("20230217") == event2.rRules.first.recur.until + "FREQ=YEARLY;UNTIL=20230218;BYMONTHDAY=15" == event2.rRules.joinToString() || + "FREQ=YEARLY;UNTIL=20230217;BYMONTHDAY=15" == event2.rRules.joinToString() ) } @Test - fun testSameTypeForDtStartAndRruleUntil_DtStartIsDateTimeAndRruleUntilIsDate() { + fun testSameTypeForDtStartAndRruleUntil_DtStartIsDateTimeWithTzAndRruleUntilIsDate() { // should add (possibly missing) time in UNTIL if DTSTART value is of type DATETIME (not just DATE) val event = Event().apply { dtStart = DtStart(DateTime("20110605T001100Z")) // DATETIME (UTC) rRules.add(RRule("FREQ=MONTHLY;UNTIL=20211214")) // DATE } - assertEquals(Date("20211214"), event.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20211214", event.rRules.joinToString()) EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) - assertEquals(DateTime("20211214T001100Z"), event.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20211214T001100Z", event.rRules.joinToString()) val event1 = Event.eventsFromReader(StringReader( "BEGIN:VCALENDAR\n" + @@ -175,17 +173,34 @@ class EventValidatorTest { "RRULE:FREQ=MONTHLY;UNTIL=20211214;BYMONTHDAY=15\n" + // DATE "END:VEVENT\n" + "END:VCALENDAR")).first() - assertEquals(DateTime("20211214T053000", tzReg.getTimeZone("America/New_York")), event1.rRules.first.recur.until) + assertEquals("FREQ=MONTHLY;UNTIL=20211214T103000Z;BYMONTHDAY=15", event1.rRules.joinToString()) + } - val event2 = Event.eventsFromReader(StringReader( - "BEGIN:VCALENDAR\n" + - "BEGIN:VEVENT\n" + - "UID:381fb26b-2da5-4dd2-94d7-2e0874128aa7\n" + - "DTSTART;VALUE=DATETIME:20080214T001100\n" + // DATETIME (no timezone) - "RRULE:FREQ=YEARLY;UNTIL=20110214;BYMONTHDAY=15\n" + // DATE - "END:VEVENT\n" + - "END:VCALENDAR")).first() - assertEquals(DateTime("20110214T001100"), event2.rRules.first.recur.until) + @Test + fun testSameTypeForDtStartAndRruleUntil_DtStartIsDateTimeWithoutTzAndRruleUntilIsDate() { + // should add (possibly missing) time in UNTIL if DTSTART value is of type DATETIME (not just DATE) + + val event = Event().apply { + dtStart = DtStart(DateTime("20110605T001100Z")) // DATETIME (UTC) + rRules.add(RRule("FREQ=MONTHLY;UNTIL=20211214")) // DATE + } + assertEquals("FREQ=MONTHLY;UNTIL=20211214", event.rRules.joinToString()) + EventValidator.sameTypeForDtStartAndRruleUntil(event.dtStart!!, event.rRules) + assertEquals("FREQ=MONTHLY;UNTIL=20211214T001100Z", event.rRules.joinToString()) + + Assume.assumeTrue(TimeZone.getDefault().id == "Europe/Vienna") + val event2 = Event.eventsFromReader( + StringReader( + "BEGIN:VCALENDAR\n" + + "BEGIN:VEVENT\n" + + "UID:381fb26b-2da5-4dd2-94d7-2e0874128aa7\n" + + "DTSTART;VALUE=DATETIME:20080214T001100\n" + // DATETIME (no timezone) + "RRULE:FREQ=YEARLY;UNTIL=20110214;BYMONTHDAY=15\n" + // DATE + "END:VEVENT\n" + + "END:VCALENDAR" + ) + ).first() + assertEquals("FREQ=YEARLY;UNTIL=20110213T231100Z;BYMONTHDAY=15", event2.rRules.joinToString()) } @@ -311,4 +326,10 @@ class EventValidatorTest { ), rrules.toTypedArray()) } + + // helpers + + private fun Iterable<RRule>.joinToString(): String = + this.map { rRule -> rRule.value }.joinToString("\n") + }
\ No newline at end of file diff --git a/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt b/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt index 398da20..aa92e57 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidCalendar.kt @@ -10,8 +10,8 @@ import android.content.ContentUris import android.content.ContentValues import android.net.Uri import android.provider.CalendarContract.* -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import java.io.FileNotFoundException import java.util.* import java.util.logging.Level diff --git a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt index 4230ee0..8648dd5 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt @@ -14,9 +14,11 @@ import android.provider.CalendarContract.* import android.util.Patterns import androidx.annotation.CallSuper import at.bitfire.ical4android.BatchOperation.CpoBuilder -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.MiscUtils.UriHelper.asSyncAdapter import at.bitfire.ical4android.util.AndroidTimeUtils +import at.bitfire.ical4android.util.DateUtils +import at.bitfire.ical4android.util.MiscUtils +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import at.bitfire.ical4android.util.TimeApiExtensions import at.bitfire.ical4android.util.TimeApiExtensions.requireZoneId import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate @@ -25,7 +27,6 @@ import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.ical4android.util.TimeApiExtensions.toLocalTime import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime -import at.bitfire.ical4android.validation.EventValidator import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.component.VAlarm @@ -780,10 +781,11 @@ abstract class AndroidEvent( builder .withValue(Events.DURATION, duration?.toRfc5545Duration(dtStart.date.toInstant())) .withValue(Events.DTEND, null) - // add RRULe - if (event.rRules.isNotEmpty()) - builder.withValue(Events.RRULE, event.rRules.joinToString(AndroidTimeUtils.RECURRENCE_RULE_SEPARATOR) { it.value }) - else + // add RRULEs + if (event.rRules.isNotEmpty()) { + builder.withValue(Events.RRULE, event.rRules + .joinToString(AndroidTimeUtils.RECURRENCE_RULE_SEPARATOR) { it.value }) + } else builder.withValue(Events.RRULE, null) if (event.rDates.isNotEmpty()) { diff --git a/src/main/java/at/bitfire/ical4android/AndroidTask.kt b/src/main/java/at/bitfire/ical4android/AndroidTask.kt index 84af609..3e56dc1 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTask.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTask.kt @@ -10,8 +10,10 @@ import android.net.Uri import android.os.RemoteException import androidx.annotation.CallSuper import at.bitfire.ical4android.BatchOperation.CpoBuilder -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues import at.bitfire.ical4android.util.AndroidTimeUtils +import at.bitfire.ical4android.util.DateUtils +import at.bitfire.ical4android.util.MiscUtils +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.TimeZone diff --git a/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt b/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt index 8439d80..01215fe 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTaskList.kt @@ -8,8 +8,8 @@ import android.accounts.Account import android.content.ContentUris import android.content.ContentValues import android.net.Uri -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues -import at.bitfire.ical4android.MiscUtils.UriHelper.asSyncAdapter +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter import org.dmfs.tasks.contract.TaskContract import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.TaskLists diff --git a/src/main/java/at/bitfire/ical4android/Event.kt b/src/main/java/at/bitfire/ical4android/Event.kt index 3f2e567..7c6449b 100644 --- a/src/main/java/at/bitfire/ical4android/Event.kt +++ b/src/main/java/at/bitfire/ical4android/Event.kt @@ -4,8 +4,8 @@ package at.bitfire.ical4android -import at.bitfire.ical4android.DateUtils.isDateTime import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME +import at.bitfire.ical4android.util.DateUtils.isDateTime import at.bitfire.ical4android.validation.EventValidator import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.data.ParserException diff --git a/src/main/java/at/bitfire/ical4android/ICalendar.kt b/src/main/java/at/bitfire/ical4android/ICalendar.kt index 678b195..c519278 100644 --- a/src/main/java/at/bitfire/ical4android/ICalendar.kt +++ b/src/main/java/at/bitfire/ical4android/ICalendar.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.util.MiscUtils import at.bitfire.ical4android.validation.ICalPreprocessor import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.data.ParserException diff --git a/src/main/java/at/bitfire/ical4android/JtxCollection.kt b/src/main/java/at/bitfire/ical4android/JtxCollection.kt index 1527ef5..dc920be 100644 --- a/src/main/java/at/bitfire/ical4android/JtxCollection.kt +++ b/src/main/java/at/bitfire/ical4android/JtxCollection.kt @@ -10,7 +10,7 @@ import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.net.Uri -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter import net.fortuna.ical4j.model.Calendar diff --git a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt index 91d53ed..fd17ed9 100644 --- a/src/main/java/at/bitfire/ical4android/JtxICalObject.kt +++ b/src/main/java/at/bitfire/ical4android/JtxICalObject.kt @@ -10,7 +10,7 @@ import android.net.ParseException import android.net.Uri import android.os.ParcelFileDescriptor import android.util.Base64 -import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues +import at.bitfire.ical4android.util.MiscUtils.CursorHelper.toValues import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.JtxICalObject.TZ_ALLDAY import at.techbee.jtx.JtxContract.asSyncAdapter diff --git a/src/main/java/at/bitfire/ical4android/Task.kt b/src/main/java/at/bitfire/ical4android/Task.kt index 0e70ca0..b19e1c6 100644 --- a/src/main/java/at/bitfire/ical4android/Task.kt +++ b/src/main/java/at/bitfire/ical4android/Task.kt @@ -5,6 +5,7 @@ package at.bitfire.ical4android import androidx.annotation.IntRange +import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.* diff --git a/src/main/java/at/bitfire/ical4android/TaskProvider.kt b/src/main/java/at/bitfire/ical4android/TaskProvider.kt index 6754388..a9a14e3 100644 --- a/src/main/java/at/bitfire/ical4android/TaskProvider.kt +++ b/src/main/java/at/bitfire/ical4android/TaskProvider.kt @@ -9,7 +9,7 @@ import android.content.ContentProviderClient import android.content.Context import android.content.pm.PackageManager import androidx.core.content.pm.PackageInfoCompat -import at.bitfire.ical4android.MiscUtils.ContentProviderClientHelper.closeCompat +import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat import org.dmfs.tasks.contract.TaskContract import java.io.Closeable import java.util.logging.Level diff --git a/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt index 9d22cd8..337ab41 100644 --- a/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -7,7 +7,6 @@ package at.bitfire.ical4android.util import android.text.format.Time -import at.bitfire.ical4android.DateUtils import at.bitfire.ical4android.Ical4Android import net.fortuna.ical4j.model.* import net.fortuna.ical4j.model.Date diff --git a/src/main/java/at/bitfire/ical4android/DateUtils.kt b/src/main/java/at/bitfire/ical4android/util/DateUtils.kt index c9ceffa..6c0060e 100644 --- a/src/main/java/at/bitfire/ical4android/DateUtils.kt +++ b/src/main/java/at/bitfire/ical4android/util/DateUtils.kt @@ -2,8 +2,10 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.ical4android +package at.bitfire.ical4android.util +import at.bitfire.ical4android.Ical4Android +import at.bitfire.ical4android.UsesThreadContextClassLoader import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime diff --git a/src/main/java/at/bitfire/ical4android/MiscUtils.kt b/src/main/java/at/bitfire/ical4android/util/MiscUtils.kt index 0c76624..76b3e02 100644 --- a/src/main/java/at/bitfire/ical4android/MiscUtils.kt +++ b/src/main/java/at/bitfire/ical4android/util/MiscUtils.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.ical4android +package at.bitfire.ical4android.util import android.accounts.Account import android.content.ContentProviderClient diff --git a/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt b/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt index f9b1ac1..9ff5529 100644 --- a/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt +++ b/src/main/java/at/bitfire/ical4android/util/TimeApiExtensions.kt @@ -4,7 +4,6 @@ package at.bitfire.ical4android.util -import at.bitfire.ical4android.DateUtils import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.util.TimeZones @@ -31,8 +30,15 @@ object TimeApiExtensions { /** * [TimeZone.toZoneId] can't be used with the current desugaring library yet! + * + * @return [ZoneId] of the time zone; [ZoneOffset.UTC] if the time zone equals to [TimeZones.getUtcTimeZone] */ - fun TimeZone.toZoneIdCompat(): ZoneId = ZoneId.of(id) + fun TimeZone.toZoneIdCompat(): ZoneId { + return if (this == TimeZones.getUtcTimeZone()) + ZoneOffset.UTC + else + ZoneId.of(id) + } /***** Dates *****/ @@ -69,6 +75,15 @@ object TimeApiExtensions { return Date(cal) } + /** + * Converts this zoned date-time (date/time with specific time zone) to an + * ical4j [DateTime] object. + * + * Sets UTC flag ([DateTime.isUtc], means `...ThhmmddZ` format) when this zone-date time object has a + * time zone of [ZoneOffset.UTC]. + * + * @return ical4j [DateTime] of the given zoned date-time + */ fun ZonedDateTime.toIcal4jDateTime(): DateTime { val date = DateTime(toEpochSecond() * MILLIS_PER_SECOND) if (zone == ZoneOffset.UTC) diff --git a/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt b/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt index d8ee48c..71e6147 100644 --- a/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt +++ b/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt @@ -4,15 +4,15 @@ package at.bitfire.ical4android.validation -import at.bitfire.ical4android.DateUtils import at.bitfire.ical4android.Event import at.bitfire.ical4android.Ical4Android import at.bitfire.ical4android.InvalidCalendarException +import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate -import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate import at.bitfire.ical4android.util.TimeApiExtensions.toZoneIdCompat import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.util.TimeZones @@ -52,16 +52,36 @@ class EventValidator(val e: Event) { */ internal fun sameTypeForDtStartAndRruleUntil(dtStart: DtStart, rRules: MutableList<RRule>) { if (DateUtils.isDate(dtStart)) { - for (rRule in rRules) { + // DTSTART is a DATE + val newRRules = mutableListOf<RRule>() + val rRuleIterator = rRules.iterator() + while (rRuleIterator.hasNext()) { + val rRule = rRuleIterator.next() rRule.recur.until?.let { until -> if (until is DateTime) { Ical4Android.log.warning("DTSTART has DATE, but UNTIL has DATETIME; making UNTIL have DATE only") - rRule.recur.until = until.toLocalDate().toIcal4jDate() + + val newUntil = until.toLocalDate().toIcal4jDate() + + // remove current RRULE and remember new one to be added + val newRRule = RRule(Recur.Builder(rRule.recur) + .until(newUntil) + .build()) + Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") + newRRules += newRRule + rRuleIterator.remove() } } } + // add repaired RRULEs + rRules += newRRules + } else if (DateUtils.isDateTime(dtStart)) { - for (rRule in rRules) { + // DTSTART is a DATE-TIME + val newRRules = mutableListOf<RRule>() + val rRuleIterator = rRules.iterator() + while (rRuleIterator.hasNext()) { + val rRule = rRuleIterator.next() rRule.recur.until?.let { until -> if (until !is DateTime) { Ical4Android.log.warning("DTSTART has DATETIME, but UNTIL has DATE; copying time from DTSTART to UNTIL") @@ -81,15 +101,30 @@ class EventValidator(val e: Event) { dtStartCal.get(Calendar.SECOND) ) - rRule.recur.until = - ZonedDateTime.of( - until.toLocalDate(), // date from until - dtStartTime, // time from dtStart - dtStartTimeZone.toZoneIdCompat() - ).toIcal4jDateTime() + val newUntil = ZonedDateTime.of( + until.toLocalDate(), // date from until + dtStartTime, // time from dtStart + dtStartTimeZone.toZoneIdCompat() + ) + + // Android requires UNTIL in UTC as defined in RFC 2445. + // https://android.googlesource.com/platform/frameworks/opt/calendar/+/refs/tags/android-12.1.0_r27/src/com/android/calendarcommon2/RecurrenceProcessor.java#93 + val newUntilUTC = DateTime(true).apply { + time = newUntil.toInstant().toEpochMilli() + } + + // remove current RRULE and remember new one to be added + val newRRule = RRule(Recur.Builder(rRule.recur) + .until(newUntilUTC) + .build()) + Ical4Android.log.info("New $newRRule (was ${rRule.toString().trim()})") + newRRules += newRRule + rRuleIterator.remove() } } } + // add repaired RRULEs + rRules += newRRules } else throw InvalidCalendarException("Event with invalid DTSTART value") } diff --git a/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt b/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt index e32ba0a..c9cf19d 100644 --- a/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt +++ b/src/test/java/at/bitfire/ical4android/MiscUtilsTest.kt @@ -4,6 +4,7 @@ package at.bitfire.ical4android +import at.bitfire.ical4android.util.MiscUtils import org.junit.Assert.assertTrue import org.junit.Test |