Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 238a3f25 authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

Introduce AndroidCompatTimeZoneRegistry (#60)

* androidify populated start times
* introduce AndroidCompatTimeZoneRegistry (closes #57, #58, #59)
parent 2c31b0e8
Loading
Loading
Loading
Loading
+70 −0
Original line number Diff line number Diff line
/***************************************************************************************************
 * 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
+16 −0
Original line number Diff line number Diff line
@@ -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 {

@@ -1459,6 +1460,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) {
+83 −0
Original line number Diff line number Diff line
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
+1 −0
Original line number Diff line number Diff line
@@ -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).
+2 −0
Original line number Diff line number Diff line
@@ -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
Loading