diff options
author | Ricki Hirner <hirner@bitfire.at> | 2022-09-11 18:47:10 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-11 18:47:10 +0300 |
commit | 238a3f25511d00559672f5f9e94d2afad8c19a18 (patch) | |
tree | f706155bacf6c2465cff87e2005ce776789c445a | |
parent | 2c31b0e8860efaa7ba5ec53b244b48463b9cd694 (diff) |
Introduce AndroidCompatTimeZoneRegistry (#60)
* androidify populated start times
* introduce AndroidCompatTimeZoneRegistry (closes #57, #58, #59)
6 files changed, 173 insertions, 0 deletions
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 556fa0b..cb094ca 100644 --- a/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt +++ b/src/androidTest/java/at/bitfire/ical4android/AndroidEventTest.kt @@ -29,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 { @@ -1460,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/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 8648dd5..7c354a6 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt @@ -228,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). diff --git a/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt b/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt index 337ab41..ec4388a 100644 --- a/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt +++ b/src/main/java/at/bitfire/ical4android/util/AndroidTimeUtils.kt @@ -104,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/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 |