diff options
author | Patrick Lang <patrick@techbee.at> | 2022-09-15 00:06:56 +0300 |
---|---|---|
committer | Patrick Lang <patrick@techbee.at> | 2022-09-15 00:06:56 +0300 |
commit | c9c6f9e8dbc8a75629bfc17f53c6bc92a0acb8a4 (patch) | |
tree | 26cb7a9dc763b5d680cd900f93491ddce8d9689b | |
parent | d28fe4d1e5b1fb8f92ff725943768ceaad202a66 (diff) | |
parent | 238a3f25511d00559672f5f9e94d2afad8c19a18 (diff) |
Merge branch 'main' into preserve-original-filename-on-syncpreserve-original-filename-on-sync
68 files changed, 507 insertions, 195 deletions
diff --git a/build.gradle b/build.gradle index a45d059..866151f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,12 @@ - /*************************************************************************************************** * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ buildscript { ext.versions = [ - kotlin: '1.7.0', + kotlin: '1.7.10', dokka: '1.5.0', - ical4j: '3.2.4', + ical4j: '3.2.5', // latest Apache Commons versions that don't require Java 8 (Android 7) commonsIO: '2.6' ] @@ -18,7 +17,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" } @@ -84,7 +83,10 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6' api("org.mnode.ical4j:ical4j:${versions.ical4j}") { + // exclude modules which are in conflict with system libraries exclude group: 'commons-logging' + exclude group: 'org.json', module: 'json' + // exclude groovy because we don't need it exclude group: 'org.codehaus.groovy', module: 'groovy' exclude group: 'org.codehaus.groovy', module: 'groovy-dateutil' } @@ -97,7 +99,7 @@ dependencies { // noinspection GradleDependency implementation "commons-io:commons-io:${versions.commonsIO}" - implementation 'org.slf4j:slf4j-jdk14:1.7.32' + implementation 'org.slf4j:slf4j-jdk14:1.7.36' implementation 'androidx.core:core-ktx:1.8.0' androidTestImplementation 'androidx.test:core:1.4.0' 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/AndroidCompatTimeZoneRegistryTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt new file mode 100644 index 0000000..5958e16 --- /dev/null +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt @@ -0,0 +1,70 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.ical4android + +import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory +import net.fortuna.ical4j.model.TimeZoneRegistry +import org.junit.Assert.* +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import java.time.ZoneId +import java.time.zone.ZoneRulesException + +class AndroidCompatTimeZoneRegistryTest { + + lateinit var ical4jRegistry: TimeZoneRegistry + lateinit var registry: TimeZoneRegistry + + val systemKnowsKyiv = + try { + ZoneId.of("Europe/Kyiv") + true + } catch (e: ZoneRulesException) { + false + } + + @Before + fun createRegistry() { + ical4jRegistry = DefaultTimeZoneRegistryFactory.getInstance().createRegistry() + registry = AndroidCompatTimeZoneRegistry.Factory().createRegistry() + } + + + @Test + fun getTimeZone_Existing() { + assertEquals( + ical4jRegistry.getTimeZone("Europe/Vienna"), + registry.getTimeZone("Europe/Vienna") + ) + } + + @Test + fun getTimeZone_Existing_Kiev() { + Assume.assumeFalse(systemKnowsKyiv) + val tz = registry.getTimeZone("Europe/Kiev") + assertFalse(tz === ical4jRegistry.getTimeZone("Europe/Kiev")) // we have made a copy + assertEquals("Europe/Kiev", tz?.id) + assertEquals("Europe/Kiev", tz?.vTimeZone?.timeZoneId?.value) + } + + @Test + fun getTimeZone_Existing_Kyiv() { + Assume.assumeFalse(systemKnowsKyiv) + + /* Unfortunately, AndroidCompatTimeZoneRegistry can't rewrite to Europy/Kyiv to anything because + it doesn't know a valid Android name for it. */ + assertEquals( + ical4jRegistry.getTimeZone("Europe/Kyiv"), + registry.getTimeZone("Europe/Kyiv") + ) + } + + @Test + fun getTimeZone_NotExisting() { + assertNull(registry.getTimeZone("Test/NotExisting")) + } + +}
\ No newline at end of file diff --git a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt index c9ff0b2..cb094ca 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.* @@ -28,6 +29,7 @@ import org.junit.Assert.* import java.net.URI import java.time.Duration import java.time.Period +import java.util.TimeZone class AndroidEventTest { @@ -1459,6 +1461,21 @@ class AndroidEventTest { } @Test + fun testPopulateEvent_NonAllDay_Recurring_Duration_KievTimeZone() { + populateEvent(false) { + put(Events.DTSTART, 1592733600000L) // 21/06/2020 18:00 +0800 + put(Events.EVENT_TIMEZONE, "Europe/Kiev") + put(Events.DURATION, "PT1H") + put(Events.RRULE, "FREQ=DAILY;COUNT=2") + }.let { result -> + assertEquals(1592733600000L, result.dtStart?.date?.time) + assertEquals(1592733600000L + 3600000, result.dtEnd?.date?.time) + assertEquals("Europe/Kiev", result.dtStart?.timeZone?.id) + assertEquals("Europe/Kiev", result.dtEnd?.timeZone?.id) + } + } + + @Test fun testPopulateEvent_NonAllDay_NonRecurring_NoTime() { populateEvent(false) { put(Events.DTSTART, 1592742600000L) // 21/06/2020 14:30 +0200 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 27debf5..4ef3148 100644 --- a/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/BatchOperationTest.kt @@ -13,15 +13,15 @@ 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 import org.junit.* -import org.junit.Assert.* - +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import java.net.URI import java.util.* diff --git a/src/test/java/at/bitfire/ical4android/EventTest.kt b/src/androidTest/java/at/bitfire/ical4android/EventTest.kt index 6842974..e016b07 100644 --- a/src/test/java/at/bitfire/ical4android/EventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/EventTest.kt @@ -1,8 +1,10 @@ /*************************************************************************************************** * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ + 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 @@ -325,8 +327,8 @@ class EventTest { } private fun parseCalendar(fname: String, charset: Charset = Charsets.UTF_8): List<Event> = - javaClass.classLoader!!.getResourceAsStream("events/$fname").use { stream -> - return Event.eventsFromReader(InputStreamReader(stream, charset)) - } + javaClass.classLoader!!.getResourceAsStream("events/$fname").use { stream -> + return Event.eventsFromReader(InputStreamReader(stream, charset)) + } } diff --git a/src/test/java/at/bitfire/ical4android/ICalPreprocessorTest.kt b/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt index e1d614b..e1d614b 100644 --- a/src/test/java/at/bitfire/ical4android/ICalPreprocessorTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/ICalPreprocessorTest.kt diff --git a/src/test/java/at/bitfire/ical4android/ICalendarTest.kt b/src/androidTest/java/at/bitfire/ical4android/ICalendarTest.kt index f2693be..a62fb72 100644 --- a/src/test/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 @@ -53,15 +54,17 @@ class ICalendarTest { @Test fun testFromReader_calendarProperties() { val calendar = ICalendar.fromReader( - StringReader("BEGIN:VCALENDAR\n" + - "VERSION:2.0\n" + - "METHOD:PUBLISH\n" + - "PRODID:something\n" + - "X-WR-CALNAME:Some Calendar\n" + - "COLOR:darkred\n" + - "X-APPLE-CALENDAR-COLOR:#123456\n" + - "END:VCALENDAR" - )) + StringReader( + "BEGIN:VCALENDAR\n" + + "VERSION:2.0\n" + + "METHOD:PUBLISH\n" + + "PRODID:something\n" + + "X-WR-CALNAME:Some Calendar\n" + + "COLOR:darkred\n" + + "X-APPLE-CALENDAR-COLOR:#123456\n" + + "END:VCALENDAR" + ) + ) assertEquals("Some Calendar", calendar.getProperty<Property>(ICalendar.CALENDAR_NAME).value) assertEquals("darkred", calendar.getProperty<Property>(Color.PROPERTY_NAME).value) assertEquals("#123456", calendar.getProperty<Property>(ICalendar.CALENDAR_COLOR).value) @@ -141,28 +144,31 @@ class ICalendarTest { @Test fun testTimezoneDefToTzId_Valid() { - assertEquals("US-Eastern", ICalendar.timezoneDefToTzId("BEGIN:VCALENDAR\n" + - "PRODID:-//Example Corp.//CalDAV Client//EN\n" + - "VERSION:2.0\n" + - "BEGIN:VTIMEZONE\n" + - "TZID:US-Eastern\n" + - "LAST-MODIFIED:19870101T000000Z\n" + - "BEGIN:STANDARD\n" + - "DTSTART:19671029T020000\n" + - "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" + - "TZOFFSETFROM:-0400\n" + - "TZOFFSETTO:-0500\n" + - "TZNAME:Eastern Standard Time (US & Canada)\n" + - "END:STANDARD\n" + - "BEGIN:DAYLIGHT\n" + - "DTSTART:19870405T020000\n" + - "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" + - "TZOFFSETFROM:-0500\n" + - "TZOFFSETTO:-0400\n" + - "TZNAME:Eastern Daylight Time (US & Canada)\n" + - "END:DAYLIGHT\n" + - "END:VTIMEZONE\n" + - "END:VCALENDAR")) + assertEquals("US-Eastern", ICalendar.timezoneDefToTzId( + "BEGIN:VCALENDAR\n" + + "PRODID:-//Example Corp.//CalDAV Client//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:US-Eastern\n" + + "LAST-MODIFIED:19870101T000000Z\n" + + "BEGIN:STANDARD\n" + + "DTSTART:19671029T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" + + "TZOFFSETFROM:-0400\n" + + "TZOFFSETTO:-0500\n" + + "TZNAME:Eastern Standard Time (US & Canada)\n" + + "END:STANDARD\n" + + "BEGIN:DAYLIGHT\n" + + "DTSTART:19870405T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" + + "TZOFFSETFROM:-0500\n" + + "TZOFFSETTO:-0400\n" + + "TZNAME:Eastern Daylight Time (US & Canada)\n" + + "END:DAYLIGHT\n" + + "END:VTIMEZONE\n" + + "END:VCALENDAR" + ) + ) } @Test @@ -171,10 +177,14 @@ class ICalendarTest { assertNull(ICalendar.timezoneDefToTzId("/* invalid content */")) // time zone without TZID - assertNull(ICalendar.timezoneDefToTzId("BEGIN:VCALENDAR\n" + - "PRODID:-//Inverse inc./SOGo 2.2.10//EN\n" + - "VERSION:2.0\n" + - "END:VCALENDAR")) + assertNull( + ICalendar.timezoneDefToTzId( + "BEGIN:VCALENDAR\n" + + "PRODID:-//Inverse inc./SOGo 2.2.10//EN\n" + + "VERSION:2.0\n" + + "END:VCALENDAR" + ) + ) } @@ -182,8 +192,9 @@ class ICalendarTest { fun testVAlarmToMin_TriggerDuration_Negative() { // TRIGGER;REL=START:-P1DT1H1M29S val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("-P1DT1H1M29S")), - Event(), false)!! + VAlarm(Duration.parse("-P1DT1H1M29S")), + Event(), false + )!! assertEquals(Related.START, ref) assertEquals(60*24 + 60 + 1, min) } @@ -192,8 +203,9 @@ class ICalendarTest { fun testVAlarmToMin_TriggerDuration_OnlySeconds() { // TRIGGER;REL=START:-PT3600S val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("-PT3600S")), - Event(), false)!! + VAlarm(Duration.parse("-PT3600S")), + Event(), false + )!! assertEquals(Related.START, ref) assertEquals(60, min) } @@ -202,8 +214,9 @@ class ICalendarTest { fun testVAlarmToMin_TriggerDuration_Positive() { // TRIGGER;REL=START:P1DT1H1M30S (alarm *after* start) val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("P1DT1H1M30S")), - Event(), false)!! + VAlarm(Duration.parse("P1DT1H1M30S")), + Event(), false + )!! assertEquals(Related.START, ref) assertEquals(-(60*24 + 60 + 1), min) } @@ -270,8 +283,8 @@ class ICalendarTest { val event = Event() event.dtStart = DtStart(Date(currentTime)) val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Period.parse("-P1W1D")), - event, false + VAlarm(Period.parse("-P1W1D")), + event, false )!! assertEquals(Related.START, ref) assertEquals(8*24*60, min) diff --git a/src/test/java/at/bitfire/ical4android/Ical4jTest.kt b/src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt index ad8d11b..7bd9292 100644 --- a/src/test/java/at/bitfire/ical4android/Ical4jTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/Ical4jTest.kt @@ -21,14 +21,18 @@ class Ical4jTest { @Test fun testEmailParameter() { // https://github.com/ical4j/ical4j/issues/418 - val e = Event.eventsFromReader(StringReader("BEGIN:VCALENDAR\n" + - "VERSION:2.0\n" + - "BEGIN:VEVENT\n" + - "SUMMARY:Test\n" + - "DTSTART;VALUE=DATE:20200702\n" + - "ATTENDEE;EMAIL=attendee1@example.com:sample:attendee1\n" + - "END:VEVENT\n" + - "END:VCALENDAR")).first() + val e = Event.eventsFromReader( + StringReader( + "BEGIN:VCALENDAR\n" + + "VERSION:2.0\n" + + "BEGIN:VEVENT\n" + + "SUMMARY:Test\n" + + "DTSTART;VALUE=DATE:20200702\n" + + "ATTENDEE;EMAIL=attendee1@example.com:sample:attendee1\n" + + "END:VEVENT\n" + + "END:VCALENDAR" + ) + ).first() assertEquals("attendee1@example.com", e.attendees.first.getParameter<Email>(Parameter.EMAIL).value) } 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/LocaleNonWesternDigitsTest.kt b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt index 81f0783..81f91c1 100644 --- a/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt @@ -5,8 +5,9 @@ package at.bitfire.ical4android import net.fortuna.ical4j.model.property.TzOffsetFrom +import org.junit.AfterClass import org.junit.Assert.assertEquals -import org.junit.Before +import org.junit.BeforeClass import org.junit.ComparisonFailure import org.junit.Test import java.time.ZoneOffset @@ -15,13 +16,22 @@ import java.util.* class LocaleNonWesternDigitsTest { companion object { - val locale = Locale("fa", "ir", "u-un-arabext") - } + val origLocale = Locale.getDefault() + val testLocale = Locale("fa", "ir", "u-un-arabext") + + @BeforeClass + @JvmStatic + fun setFaIrArabLocale() { + assertEquals("Persian (Iran) locale not available", "fa", testLocale.language) + Locale.setDefault(testLocale) + } + + @AfterClass + @JvmStatic + fun resetLocale() { + Locale.setDefault(origLocale) + } - @Before - fun verifyLocale() { - assertEquals("Persian (Iran) locale not available", "fa", locale.language) - Locale.setDefault(locale) } @Test diff --git a/src/test/java/at/bitfire/ical4android/TaskTest.kt b/src/androidTest/java/at/bitfire/ical4android/TaskTest.kt index cbe0bcc..f9f72af 100644 --- a/src/test/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/TestEvent.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt index f9c9571..11270a1 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestEvent.kt @@ -5,7 +5,6 @@ package at.bitfire.ical4android.impl import android.content.ContentValues -import android.provider.CalendarContract import android.provider.CalendarContract.Events import at.bitfire.ical4android.* import java.util.* 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/impl/TestJtxIcalObject.kt b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt index 57615fb..a1397e9 100644 --- a/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt +++ b/src/androidTest/java/at/bitfire/ical4android/impl/TestJtxIcalObject.kt @@ -5,7 +5,9 @@ package at.bitfire.ical4android.impl import android.content.ContentValues -import at.bitfire.ical4android.* +import at.bitfire.ical4android.JtxCollection +import at.bitfire.ical4android.JtxICalObject +import at.bitfire.ical4android.JtxICalObjectFactory class TestJtxIcalObject(testCollection: JtxCollection<JtxICalObject>): JtxICalObject(testCollection) { diff --git a/src/test/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt index 2c68b95..18a37f9 100644 --- a/src/test/java/at/bitfire/ical4android/AndroidTimeUtilsTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/util/AndroidTimeUtilsTest.kt @@ -2,13 +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.util.AndroidTimeUtils import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.* -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.TzId import net.fortuna.ical4j.model.parameter.Value @@ -19,11 +16,9 @@ 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 -import java.util.* class AndroidTimeUtilsTest { diff --git a/src/test/java/at/bitfire/ical4android/DateUtilsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/DateUtilsTest.kt index 395b8da..fe1f468 100644 --- a/src/test/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/test/java/at/bitfire/ical4android/TimeApiExtensionsTest.kt b/src/androidTest/java/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt index 44a02a0..e5c6fc0 100644 --- a/src/test/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/test/java/at/bitfire/ical4android/validation/EventValidatorTest.kt b/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt index c64d0d7..fa3d959 100644 --- a/src/test/java/at/bitfire/ical4android/validation/EventValidatorTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/validation/EventValidatorTest.kt @@ -6,44 +6,42 @@ 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 -import java.util.* class EventValidatorTest { - val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() + companion object { + val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() + } // DTSTART and DTEND - @Test - fun testEnsureCorrectStartAndEndTime_noDtStart() { - assertThrows(InvalidCalendarException::class.java) { - val event = Event().apply { - dtEnd = DtEnd(DateTime("20000105T000000")) // DATETIME - // no dtStart - } - EventValidator.correctStartAndEndTime(event) + @Test(expected = InvalidCalendarException::class) + fun testEnsureCorrectStartAndEndTime_noDtStart_DateTime() { + val event = Event().apply { + dtEnd = DtEnd(DateTime("20000105T000000")) // DATETIME + // no dtStart } + EventValidator.correctStartAndEndTime(event) + } - assertThrows(InvalidCalendarException::class.java) { - Event.eventsFromReader(StringReader( - "BEGIN:VCALENDAR\n" + - "BEGIN:VEVENT\n" + - "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + - "DTEND;VALUE=DATE:20211116\n" + // DATE - "END:VEVENT\n" + - "END:VCALENDAR")).first() - } + @Test(expected = InvalidCalendarException::class) + fun testEnsureCorrectStartAndEndTime_noDtStart_Date() { + Event.eventsFromReader(StringReader( + "BEGIN:VCALENDAR\n" + + "BEGIN:VEVENT\n" + + "UID:51d8529a-5844-4609-918b-2891b855e0e8\n" + + "DTEND;VALUE=DATE:20211116\n" + // DATE + "END:VEVENT\n" + + "END:VCALENDAR")).first() } @Test @@ -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/test/resources/events/all-day-0sec.ics b/src/androidTest/resources/events/all-day-0sec.ics index 07679f2..07679f2 100644 --- a/src/test/resources/events/all-day-0sec.ics +++ b/src/androidTest/resources/events/all-day-0sec.ics diff --git a/src/test/resources/events/all-day-10days.ics b/src/androidTest/resources/events/all-day-10days.ics index 52e6dbd..52e6dbd 100644 --- a/src/test/resources/events/all-day-10days.ics +++ b/src/androidTest/resources/events/all-day-10days.ics diff --git a/src/test/resources/events/all-day-1day.ics b/src/androidTest/resources/events/all-day-1day.ics index 506d295..506d295 100644 --- a/src/test/resources/events/all-day-1day.ics +++ b/src/androidTest/resources/events/all-day-1day.ics diff --git a/src/test/resources/events/dst-only-vtimezone.ics b/src/androidTest/resources/events/dst-only-vtimezone.ics index e49d74d..e49d74d 100644 --- a/src/test/resources/events/dst-only-vtimezone.ics +++ b/src/androidTest/resources/events/dst-only-vtimezone.ics diff --git a/src/test/resources/events/event-on-that-day.ics b/src/androidTest/resources/events/event-on-that-day.ics index 0ccbd4f..0ccbd4f 100644 --- a/src/test/resources/events/event-on-that-day.ics +++ b/src/androidTest/resources/events/event-on-that-day.ics diff --git a/src/test/resources/events/latin1.ics b/src/androidTest/resources/events/latin1.ics index 20d97d0..20d97d0 100644 --- a/src/test/resources/events/latin1.ics +++ b/src/androidTest/resources/events/latin1.ics diff --git a/src/test/resources/events/multiple.ics b/src/androidTest/resources/events/multiple.ics index 6e300ab..6e300ab 100644 --- a/src/test/resources/events/multiple.ics +++ b/src/androidTest/resources/events/multiple.ics diff --git a/src/test/resources/events/one-event-with-exception-one-without.ics b/src/androidTest/resources/events/one-event-with-exception-one-without.ics index 182711b..182711b 100644 --- a/src/test/resources/events/one-event-with-exception-one-without.ics +++ b/src/androidTest/resources/events/one-event-with-exception-one-without.ics diff --git a/src/test/resources/events/one-event-with-multiple-exceptions-one-without.ics b/src/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics index bff4528..bff4528 100644 --- a/src/test/resources/events/one-event-with-multiple-exceptions-one-without.ics +++ b/src/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics diff --git a/src/test/resources/events/outlook1.ics b/src/androidTest/resources/events/outlook1.ics index 000afaa..000afaa 100644 --- a/src/test/resources/events/outlook1.ics +++ b/src/androidTest/resources/events/outlook1.ics diff --git a/src/test/resources/events/recurring-only-exception.ics b/src/androidTest/resources/events/recurring-only-exception.ics index 26012e1..26012e1 100644 --- a/src/test/resources/events/recurring-only-exception.ics +++ b/src/androidTest/resources/events/recurring-only-exception.ics diff --git a/src/test/resources/events/recurring-with-exception1.ics b/src/androidTest/resources/events/recurring-with-exception1.ics index 0382497..0382497 100644 --- a/src/test/resources/events/recurring-with-exception1.ics +++ b/src/androidTest/resources/events/recurring-with-exception1.ics diff --git a/src/test/resources/events/two-events-without-exceptions.ics b/src/androidTest/resources/events/two-events-without-exceptions.ics index be6290f..be6290f 100644 --- a/src/test/resources/events/two-events-without-exceptions.ics +++ b/src/androidTest/resources/events/two-events-without-exceptions.ics diff --git a/src/test/resources/events/two-line-description-without-crlf.ics b/src/androidTest/resources/events/two-line-description-without-crlf.ics index 03c8b62..03c8b62 100644 --- a/src/test/resources/events/two-line-description-without-crlf.ics +++ b/src/androidTest/resources/events/two-line-description-without-crlf.ics diff --git a/src/test/resources/events/utf8.ics b/src/androidTest/resources/events/utf8.ics index 87171a9..87171a9 100644 --- a/src/test/resources/events/utf8.ics +++ b/src/androidTest/resources/events/utf8.ics diff --git a/src/test/resources/events/vienna-evolution.ics b/src/androidTest/resources/events/vienna-evolution.ics index 5f11911..5f11911 100644 --- a/src/test/resources/events/vienna-evolution.ics +++ b/src/androidTest/resources/events/vienna-evolution.ics diff --git a/src/test/resources/tasks/empty-priority.ics b/src/androidTest/resources/tasks/empty-priority.ics index 69ad1ae..69ad1ae 100644 --- a/src/test/resources/tasks/empty-priority.ics +++ b/src/androidTest/resources/tasks/empty-priority.ics diff --git a/src/test/resources/tasks/latin1.ics b/src/androidTest/resources/tasks/latin1.ics index b3502de..b3502de 100644 --- a/src/test/resources/tasks/latin1.ics +++ b/src/androidTest/resources/tasks/latin1.ics diff --git a/src/test/resources/tasks/most-fields1.ics b/src/androidTest/resources/tasks/most-fields1.ics index ff4dbb0..ff4dbb0 100644 --- a/src/test/resources/tasks/most-fields1.ics +++ b/src/androidTest/resources/tasks/most-fields1.ics diff --git a/src/test/resources/tasks/most-fields2.ics b/src/androidTest/resources/tasks/most-fields2.ics index f6c7215..f6c7215 100644 --- a/src/test/resources/tasks/most-fields2.ics +++ b/src/androidTest/resources/tasks/most-fields2.ics diff --git a/src/test/resources/tasks/rfc5545-sample1.ics b/src/androidTest/resources/tasks/rfc5545-sample1.ics index 18d53df..18d53df 100644 --- a/src/test/resources/tasks/rfc5545-sample1.ics +++ b/src/androidTest/resources/tasks/rfc5545-sample1.ics diff --git a/src/test/resources/tasks/utf8.ics b/src/androidTest/resources/tasks/utf8.ics index 67aaa1e..67aaa1e 100644 --- a/src/test/resources/tasks/utf8.ics +++ b/src/androidTest/resources/tasks/utf8.ics diff --git a/src/test/resources/tz/Karachi.ics b/src/androidTest/resources/tz/Karachi.ics index 1515d2b..1515d2b 100644 --- a/src/test/resources/tz/Karachi.ics +++ b/src/androidTest/resources/tz/Karachi.ics diff --git a/src/test/resources/tz/Mogadishu.ics b/src/androidTest/resources/tz/Mogadishu.ics index b5e4fd3..b5e4fd3 100644 --- a/src/test/resources/tz/Mogadishu.ics +++ b/src/androidTest/resources/tz/Mogadishu.ics diff --git a/src/test/resources/tz/Vienna.ics b/src/androidTest/resources/tz/Vienna.ics index 5b0f65f..5b0f65f 100644 --- a/src/test/resources/tz/Vienna.ics +++ b/src/androidTest/resources/tz/Vienna.ics 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/AndroidCompatTimeZoneRegistry.kt b/src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt new file mode 100644 index 0000000..5f7246d --- /dev/null +++ b/src/main/java/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt @@ -0,0 +1,83 @@ +package at.bitfire.ical4android + +import net.fortuna.ical4j.model.* +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.property.TzId +import java.time.ZoneId + +/** + * The purpose of this class is that if a time zone has a different name in ical4j and Android, + * it should use the Android name. + * + * For instance, if a time zone is known as "Europe/Kyiv" (with alias "Europe/Kiev") in ical4j + * and only "Europe/Kiev" in Android, this registry behaves like the default [TimeZoneRegistryImpl], + * but the returned time zone for `getTimeZone("Europe/Kiev")` has an ID of "Europe/Kiev" and not + * "Europe/Kyiv". + */ +class AndroidCompatTimeZoneRegistry( + private val base: TimeZoneRegistry +): TimeZoneRegistry by base { + + /** + * Gets the time zone for a given ID. + * + * If a time zone with the given ID exists in Android, the icalj timezone for this ID + * is returned, but the TZID is set to the Android name (and not the ical4j name, which + * may not be known to Android). + * + * If a time zone with the given ID doesn't exist in Android, this method returns the + * result of its [base] method. + * + * @param id + * @return time zone + */ + override fun getTimeZone(id: String): TimeZone? { + // check whether time zone is available on Android + val androidTzId = + try { + ZoneId.of(id).id + } catch (e: Exception) { + /* Not available in Android, should return null in a later version. + However, we return the ical4j timezone to keep the changes caused by AndroidCompatTimeZoneRegistry introduction + as small as possible. */ + return base.getTimeZone(id) + } + + /* Time zone known by Android. Unfortunately, we can't use the Android timezone database directly + to generate ical4j timezone definitions (which are based on VTIMEZONE). + So we have to use the timezone definition from ical4j (based on its own VTIMEZONE database), + but we also need to use the Android TZ name (otherwise Android may not understand it later). + + Example: getTimeZone("Europe/Kiev") returns a TimeZone with TZID:Europe/Kyiv since ical4j/3.2.5, + but most Android devices don't now Europe/Kyiv yet. + */ + val tz = base.getTimeZone(id) + if (tz.id != androidTzId) { + Ical4Android.log.warning("Using Android TZID $androidTzId instead of ical4j ${tz.id}") + + // create a copy of the VTIMEZONE so that we don't modify the original registry values (which are not immutable) + val vTimeZone = tz.vTimeZone + val newVTimeZoneProperties = PropertyList(vTimeZone.properties) + newVTimeZoneProperties.removeAll { property -> + property is TzId + } + newVTimeZoneProperties += TzId(androidTzId) + return TimeZone(VTimeZone( + newVTimeZoneProperties, + vTimeZone.observances + )) + } else + return tz + } + + + class Factory : TimeZoneRegistryFactory() { + + override fun createRegistry(): TimeZoneRegistry { + val ical4jRegistry = DefaultTimeZoneRegistryFactory().createRegistry() + return AndroidCompatTimeZoneRegistry(ical4jRegistry) + } + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt index 4230ee0..7c354a6 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 @@ -227,6 +228,7 @@ abstract class AndroidEvent( } } event.dtStart = DtStart(dtStartDateTime) + AndroidTimeUtils.androidifyTimeZone(event.dtStart) // because it may have an ical4j timezone ID that is not available in Android // Android events MUST have duration or dtend [https://developer.android.com/reference/android/provider/CalendarContract.Events#operations]. // Assume 1 hour if missing (should never occur, but occurs). @@ -780,10 +782,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 b35f733..9ffd5d3 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..ec4388a 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 @@ -105,6 +104,8 @@ object AndroidTimeUtils { * Returns the time-zone ID for a given date or date-time that should be used to store it * in the Android calendar provider. * + * Does not check whether Android actually knows the time zone ID – use [androidifyTimeZone] for that. + * * @param date DateProperty (DATE or DATE-TIME) whose time-zone information is used * * @return - UTC for dates and UTC date-times 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 ac0e108..71e6147 100644 --- a/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt +++ b/src/main/java/at/bitfire/ical4android/validation/EventValidator.kt @@ -4,19 +4,21 @@ 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.Date 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 java.time.* +import net.fortuna.ical4j.util.TimeZones +import java.time.LocalTime +import java.time.ZonedDateTime +import java.util.* /** * Sometimes CalendarStorage or servers respond with invalid event definitions. Here we try to @@ -50,34 +52,79 @@ 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") - val timeZone = if (dtStart.timeZone != null) - dtStart.timeZone.toZoneIdCompat() + val dtStartTimeZone = if (dtStart.timeZone != null) + dtStart.timeZone else if (dtStart.isUtc) - ZoneOffset.UTC + TimeZones.getUtcTimeZone() else /* floating time */ - ZoneId.systemDefault() - rRule.recur.until = - ZonedDateTime.of( - until.toLocalDate(), // date from until - LocalTime.ofInstant(dtStart.date.toInstant(), timeZone), // time from dtStart - timeZone - ).toIcal4jDateTime() + TimeZone.getDefault() + + val dtStartCal = Calendar.getInstance(dtStartTimeZone).apply { + time = dtStart.date + } + val dtStartTime = LocalTime.of( + dtStartCal.get(Calendar.HOUR_OF_DAY), + dtStartCal.get(Calendar.MINUTE), + dtStartCal.get(Calendar.SECOND) + ) + + 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/main/resources/ical4j.properties b/src/main/resources/ical4j.properties index dfa5830..5a83b9f 100644 --- a/src/main/resources/ical4j.properties +++ b/src/main/resources/ical4j.properties @@ -1,4 +1,5 @@ net.fortuna.ical4j.timezone.cache.impl=net.fortuna.ical4j.util.MapTimeZoneCache +net.fortuna.ical4j.timezone.registry=at.bitfire.ical4android.AndroidCompatTimeZoneRegistry$Factory net.fortuna.ical4j.timezone.update.enabled=false ical4j.parsing.relaxed=true ical4j.unfolding.relaxed=true diff --git a/src/test/README.txt b/src/test/README.txt new file mode 100644 index 0000000..60a244a --- /dev/null +++ b/src/test/README.txt @@ -0,0 +1,6 @@ + +ATTENTION! + +Do not use JVM unit tests for methods the use the Time API or other APIs that behave +differently in normal JVMs and the Android JVM. Especially do not write JVM unit tests +that use APIs that are desugared in Android. Use Android unit tests (androidTest) instead. 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 |