From 9d8a17dc83215537be483699c58d8f8a354dbf6d Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Wed, 4 Mar 2026 12:38:19 +0100 Subject: [PATCH 01/24] Update ical4j to 4.x and make everything compile again (#197) * Update ical4j dependency * Make at.bitfire.ical4android package compile again * Make package at.bitfire.synctools.icalendar compile again * Make at.bitfire.synctools compile again (add TODOs) * Make at.bitfire.ical4android unit tests run again * Make event builder tests run again * Make remaining unit tests compile again --- gradle/libs.versions.toml | 3 +- .../AndroidCompatTimeZoneRegistryTest.kt | 5 +- .../at/bitfire/ical4android/DmfsTaskTest.kt | 16 ++- .../bitfire/ical4android/JtxICalObjectTest.kt | 5 +- .../mapping/tasks/DmfsTaskBuilderTest.kt | 10 +- .../AndroidCompatTimeZoneRegistry.kt | 13 +- .../at/bitfire/ical4android/ICalendar.kt | 41 +++--- .../at/bitfire/ical4android/JtxCollection.kt | 8 +- .../at/bitfire/ical4android/JtxICalObject.kt | 120 ++++-------------- .../kotlin/at/bitfire/ical4android/Task.kt | 18 +-- .../at/bitfire/ical4android/TaskReader.kt | 35 +---- .../at/bitfire/ical4android/TaskWriter.kt | 32 +---- .../bitfire/ical4android/UnknownProperty.kt | 6 +- .../at/bitfire/ical4android/util/DateUtils.kt | 13 +- .../icalendar/CalendarUidSplitter.kt | 12 +- .../synctools/icalendar/ICalendarGenerator.kt | 10 +- .../synctools/icalendar/ICalendarParser.kt | 6 +- .../synctools/icalendar/Ical4jHelpers.kt | 30 +++-- .../icalendar/validation/ICalPreprocessor.kt | 19 +-- .../mapping/calendar/AndroidEventHandler.kt | 17 ++- .../mapping/calendar/AttendeeMappings.kt | 14 +- .../calendar/builder/AccessLevelBuilder.kt | 4 +- .../mapping/calendar/builder/AllDayBuilder.kt | 5 +- .../calendar/builder/AttendeesBuilder.kt | 8 +- .../calendar/builder/AvailabilityBuilder.kt | 5 +- .../calendar/builder/CategoriesBuilder.kt | 5 +- .../mapping/calendar/builder/ColorBuilder.kt | 5 +- .../calendar/builder/DurationBuilder.kt | 19 +-- .../calendar/builder/EndTimeBuilder.kt | 26 ++-- .../calendar/builder/OrganizerBuilder.kt | 5 +- .../builder/OriginalInstanceTimeBuilder.kt | 7 +- .../builder/RecurrenceFieldsBuilder.kt | 5 +- .../calendar/builder/RemindersBuilder.kt | 5 +- .../calendar/builder/StartTimeBuilder.kt | 5 +- .../mapping/calendar/builder/StatusBuilder.kt | 5 +- .../mapping/calendar/builder/UidBuilder.kt | 3 +- .../builder/UnknownPropertiesBuilder.kt | 5 +- .../calendar/handler/AccessLevelHandler.kt | 7 +- .../calendar/handler/AttendeesHandler.kt | 5 +- .../calendar/handler/AvailabilityHandler.kt | 5 +- .../calendar/handler/CategoriesHandler.kt | 5 +- .../mapping/calendar/handler/ColorHandler.kt | 5 +- .../calendar/handler/DescriptionHandler.kt | 5 +- .../calendar/handler/DurationHandler.kt | 5 +- .../calendar/handler/EndTimeHandler.kt | 3 +- .../calendar/handler/LocationHandler.kt | 5 +- .../calendar/handler/OrganizerHandler.kt | 3 +- .../handler/OriginalInstanceTimeHandler.kt | 5 +- .../handler/RecurrenceFieldsHandler.kt | 14 +- .../calendar/handler/RemindersHandler.kt | 5 +- .../calendar/handler/SequenceHandler.kt | 5 +- .../calendar/handler/StartTimeHandler.kt | 3 +- .../mapping/calendar/handler/StatusHandler.kt | 5 +- .../mapping/calendar/handler/TitleHandler.kt | 5 +- .../mapping/calendar/handler/UidHandler.kt | 5 +- .../handler/UnknownPropertiesHandler.kt | 5 +- .../mapping/calendar/handler/UrlHandler.kt | 5 +- .../mapping/tasks/DmfsTaskBuilder.kt | 24 ++-- .../mapping/tasks/DmfsTaskProcessor.kt | 21 +-- .../synctools/util/AndroidTimeUtils.kt | 74 ++++++----- .../main/kotlin/at/techbee/jtx/JtxContract.kt | 27 ++-- .../at/bitfire/ical4android/ICalendarTest.kt | 23 +++- .../ical4android/Ical4jServiceLoaderTest.kt | 3 +- .../at/bitfire/ical4android/TaskReaderTest.kt | 23 ++-- .../at/bitfire/ical4android/TaskTest.kt | 5 +- .../at/bitfire/ical4android/TaskWriterTest.kt | 5 +- .../ical4android/UnknownPropertyTest.kt | 10 +- .../ical4android/util/DateUtilsTest.kt | 10 +- .../icalendar/AssociatedComponentsTest.kt | 8 +- .../icalendar/CalendarUidSplitterTest.kt | 11 +- .../icalendar/ICalendarGeneratorTest.kt | 5 +- .../bitfire/synctools/icalendar/Ical4jTest.kt | 26 ++-- .../validation/ICalPreprocessorTest.kt | 5 +- .../calendar/AndroidEventHandlerTest.kt | 8 +- .../mapping/calendar/AttendeeMappingsTest.kt | 7 +- .../builder/AccessLevelBuilderTest.kt | 15 ++- .../calendar/builder/AllDayBuilderTest.kt | 10 +- .../calendar/builder/AttendeesBuilderTest.kt | 8 +- .../builder/AvailabilityBuilderTest.kt | 10 +- .../calendar/builder/CategoriesBuilderTest.kt | 3 +- .../calendar/builder/DurationBuilderTest.kt | 8 +- .../calendar/builder/EndTimeBuilderTest.kt | 8 +- .../calendar/builder/OrganizerBuilderTest.kt | 9 +- .../OriginalInstanceTimeBuilderTest.kt | 8 +- .../builder/RecurrenceFieldsBuilderTest.kt | 8 +- .../calendar/builder/RemindersBuilderTest.kt | 8 +- .../calendar/builder/StartTimeBuilderTest.kt | 8 +- .../calendar/builder/StatusBuilderTest.kt | 8 +- .../builder/UnknownPropertiesBuilderTest.kt | 5 +- .../handler/AccessLevelHandlerTest.kt | 9 +- .../calendar/handler/AttendeesHandlerTest.kt | 8 +- .../handler/AvailabilityHandlerTest.kt | 3 +- .../calendar/handler/CategoriesHandlerTest.kt | 3 +- .../calendar/handler/ColorHandlerTest.kt | 6 +- .../calendar/handler/DurationHandlerTest.kt | 8 +- .../calendar/handler/EndTimeHandlerTest.kt | 8 +- .../OriginalInstanceTimeHandlerTest.kt | 8 +- .../handler/RecurrenceFieldHandlerTest.kt | 8 +- .../calendar/handler/RemindersHandlerTest.kt | 8 +- .../calendar/handler/StartTimeHandlerTest.kt | 6 +- .../calendar/handler/StatusHandlerTest.kt | 9 +- .../calendar/handler/UidHandlerTest.kt | 3 +- .../handler/UnknownPropertiesHandlerTest.kt | 8 +- .../synctools/util/AndroidTimeUtilsTest.kt | 13 +- 104 files changed, 641 insertions(+), 533 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f298b7a..1159f3d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,8 +8,7 @@ androidx-test-runner = "1.7.0" dokka = "2.1.0" ezvcard = "0.12.2" guava = "33.5.0-android" -# noinspection NewerVersionAvailable -ical4j = "3.2.19" # final version; update to 4.x will require much work +ical4j = "4.2.3" junit = "4.13.2" kotlin = "2.3.10" mockk = "1.14.9" diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt index 43a763ba..5663ea5e 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt @@ -48,14 +48,15 @@ class AndroidCompatTimeZoneRegistryTest { @Test fun getTimeZone_Existing_ButNotInIcal4j() { - val reg = AndroidCompatTimeZoneRegistry(object: TimeZoneRegistry { + TODO("ical4j 4.x") + /*val reg = AndroidCompatTimeZoneRegistry(object: TimeZoneRegistry { override fun register(timezone: TimeZone?) = throw NotImplementedError() override fun register(timezone: TimeZone?, update: Boolean) = throw NotImplementedError() override fun clear() = throw NotImplementedError() override fun getTimeZone(id: String?) = null }) - assertNull(reg.getTimeZone("Europe/Berlin")) + assertNull(reg.getTimeZone("Europe/Berlin"))*/ } @Test diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index a6d3fa5d..4ae45c94 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -82,7 +82,8 @@ class DmfsTaskTest( @Test fun testAddTask() { // build and write event to calendar provider - val task = Task() + TODO("ical4j 4.x") + /*val task = Task() task.uid = "sample1@testAddEvent" task.summary = "Sample event" task.description = "Sample event with date/time" @@ -125,18 +126,19 @@ class DmfsTaskTest( assertEquals(task.unknownProperties, task2.unknownProperties) } finally { testTask.delete() - } + }*/ } @Test(expected = LocalStorageException::class) fun testAddTaskWithInvalidDue() { val task = Task() - task.uid = "invalidDUE@ical4android.tests" + TODO("ical4j 4.x") + /*task.uid = "invalidDUE@ical4android.tests" task.summary = "Task with invalid DUE" task.dtStart = DtStart(Date("20150102")) task.due = Due(Date("20150101")) - DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() + DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add()*/ } @Test @@ -144,7 +146,8 @@ class DmfsTaskTest( val task = Task() task.uid = "TaskWithManyAlarms" task.summary = "Task with many alarms" - task.dtStart = DtStart(Date("20150102")) + TODO("ical4j 4.x") + //task.dtStart = DtStart(Date("20150102")) for (i in 1..1050) task.alarms += VAlarm(java.time.Duration.ofMinutes(i.toLong())) @@ -162,7 +165,8 @@ class DmfsTaskTest( task.summary = "Sample event" task.description = "Sample event with date/time" task.location = "Sample location" - task.dtStart = DtStart("20150501T120000", tzVienna) + TODO("ical4j 4.x") + //task.dtStart = DtStart("20150501T120000", tzVienna) assertFalse(task.isAllDay()) val uri = DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() assertNotNull(uri) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt index cff10a33..ea4fb928 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt @@ -837,7 +837,8 @@ class JtxICalObjectTest { //assertEquals(iCalIn.components[0].getProperty(Component.VTODO), iCalOut.components[0].getProperty(Component.VTODO)) // there should only be one component for VJOURNAL and VTODO! - for(i in 0 until iCalIn.components.size) { + TODO("ical4j 4.x") + /*for(i in 0 until iCalIn.components.size) { iCalIn.components[i].properties.forEach { inProp -> @@ -846,7 +847,7 @@ class JtxICalObjectTest { val outProp = iCalOut.components[i].properties.getProperty(inProp.name) assertEquals(inProp, outProp) } - } + }*/ } diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt index 8e467979..e8e884d1 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt @@ -82,7 +82,11 @@ class DmfsTaskBuilderTest ( // builder tests - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun testBuildTask_Sequence() { buildTask { ICalendar.apply { sequence = 12345 } @@ -263,7 +267,7 @@ class DmfsTaskBuilderTest ( buildTask { }.let { result -> assertEquals( - TaskContract.Tasks.CLASSIFICATION_DEFAULT /* null */, + TaskContract.Tasks.CLASSIFICATION_DEFAULT *//* null *//*, result.getAsInteger(TaskContract.Tasks.CLASSIFICATION) ) } @@ -771,7 +775,7 @@ class DmfsTaskBuilderTest ( val dmfsTask = DmfsTask(taskList!!, task, "9dc64544-1816-4f04-b952-e894164467f6", null, 0) dmfsTask.task!!.dtStart = DtStart("20150101", tzVienna) assertEquals(tzVienna, builder.getTimeZone()) - } + }*/ // helpers diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt index 4d71c40b..97a4e907 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt @@ -6,17 +6,13 @@ package at.bitfire.ical4android -import java.time.ZoneId -import java.util.logging.Logger import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory -import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistry import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.TimeZoneRegistryImpl -import net.fortuna.ical4j.model.component.VTimeZone -import net.fortuna.ical4j.model.property.TzId +import java.time.ZoneId +import java.util.logging.Logger /** * Wrapper around default [TimeZoneRegistry] that uses the Android name if a time zone has a @@ -78,12 +74,13 @@ class AndroidCompatTimeZoneRegistry( // 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() + TODO("ical4j 4.x") + /*val newVTimeZoneProperties = PropertyList() newVTimeZoneProperties += TzId(androidTzId) return TimeZone(VTimeZone( newVTimeZoneProperties, vTimeZone.observances - )) + ))*/ } else return tz } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index 36fdfbb7..0a85a361 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -11,31 +11,17 @@ import at.bitfire.synctools.BuildConfig import at.bitfire.synctools.exception.InvalidICalendarException import at.bitfire.synctools.icalendar.ICalendarParser import at.bitfire.synctools.icalendar.validation.ICalPreprocessor -import net.fortuna.ical4j.data.CalendarBuilder -import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.Calendar -import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.PropertyList -import net.fortuna.ical4j.model.component.Daylight import net.fortuna.ical4j.model.component.Observance -import net.fortuna.ical4j.model.component.Standard import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId -import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.validate.ValidationException import java.io.Reader -import java.io.StringReader -import java.time.Duration -import java.time.Period import java.util.LinkedList import java.util.UUID import java.util.logging.Level @@ -102,7 +88,8 @@ open class ICalendar { // fill calendar properties properties?.let { - calendar.getProperty(CALENDAR_NAME)?.let { calName -> + TODO("ical4j 4.x") + /*calendar.getProperty(CALENDAR_NAME)?.let { calName -> properties[CALENDAR_NAME] = calName.value } @@ -111,7 +98,7 @@ open class ICalendar { } calendar.getProperty(CALENDAR_COLOR)?.let { calColor -> properties[CALENDAR_COLOR] = calColor.value - } + }*/ } return calendar @@ -137,6 +124,10 @@ open class ICalendar { var newTz: VTimeZone? = null val keep = mutableSetOf() + TODO("ical4j 4.x") + // Note: big method – maybe split? + + /* if (start != null) { // find latest matching STANDARD/DAYLIGHT observances var latestDaylight: Pair? = null @@ -210,7 +201,7 @@ open class ICalendar { logger.log(Level.WARNING, "Minified timezone is invalid, using original one", e) newTz = null } - } + }*/ // use original time zone if we couldn't calculate a minified one return newTz ?: originalTz @@ -222,14 +213,15 @@ open class ICalendar { * @return time zone id (TZID) if VTIMEZONE contains a TZID, null otherwise */ fun timezoneDefToTzId(timezoneDef: String): String? { - try { + TODO("ical4j 4.x") + /*try { val builder = CalendarBuilder() val cal = builder.build(StringReader(timezoneDef)) val timezone = cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone? timezone?.timeZoneId?.let { return it.value } } catch (e: ParserException) { logger.log(Level.SEVERE, "Can't understand time zone definition", e) - } + }*/ return null } @@ -279,14 +271,17 @@ open class ICalendar { */ fun vAlarmToMin( alarm: VAlarm, - refStart: DtStart?, - refEnd: DateProperty?, + refStart: DtStart<*>?, + refEnd: DateProperty<*>?, refDuration: net.fortuna.ical4j.model.property.Duration?, allowRelEnd: Boolean ): Pair? { val trigger = alarm.trigger ?: return null - val minutes: Int // minutes before/after the event + TODO("ical4j 4.x") + // Note: big method – maybe split? + + /*val minutes: Int // minutes before/after the event var related = trigger.getParameter(Parameter.RELATED) ?: Related.START // event/task start time @@ -345,7 +340,7 @@ open class ICalendar { return null } - return Pair(related, minutes) + return Pair(related, minutes)*/ } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index 748e30b8..a6330bb1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -17,10 +17,7 @@ import at.bitfire.synctools.storage.toContentValues import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter import net.fortuna.ical4j.model.Calendar -import net.fortuna.ical4j.model.component.VJournal -import net.fortuna.ical4j.model.component.VToDo import net.fortuna.ical4j.model.property.ProdId -import net.fortuna.ical4j.model.property.Version import java.util.LinkedList import java.util.logging.Level import java.util.logging.Logger @@ -259,7 +256,8 @@ open class JtxCollection(val account: Account, logger.fine("getICSForCollection: found ${cursor?.count} records in ${account.name}") val ical = Calendar() - ical.properties += Version.VERSION_2_0 + TODO("ical4j 4.x") + /*ical.properties += Version.VERSION_2_0 ical.properties += prodId while (cursor?.moveToNext() == true) { @@ -270,7 +268,7 @@ open class JtxCollection(val account: Account, if(component is VToDo || component is VJournal) ical.components += component } - } + }*/ return ical.toString() } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt index 545aace4..fcddc3cb 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt @@ -6,100 +6,26 @@ package at.bitfire.ical4android -import android.content.ContentUris import android.content.ContentValues -import android.net.ParseException import android.net.Uri import android.os.ParcelFileDescriptor import android.util.Base64 import androidx.core.content.contentValuesOf -import at.bitfire.ical4android.ICalendar.Companion.withUserAgents import at.bitfire.synctools.exception.InvalidICalendarException -import at.bitfire.synctools.icalendar.Css3Color import at.bitfire.synctools.storage.BatchOperation import at.bitfire.synctools.storage.JtxBatchOperation import at.bitfire.synctools.storage.toContentValues import at.techbee.jtx.JtxContract -import at.techbee.jtx.JtxContract.JtxICalObject.TZ_ALLDAY import at.techbee.jtx.JtxContract.asSyncAdapter import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.ComponentList -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateList -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.ParameterList -import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyList -import net.fortuna.ical4j.model.TextList -import net.fortuna.ical4j.model.TimeZoneRegistryFactory -import net.fortuna.ical4j.model.component.VAlarm -import net.fortuna.ical4j.model.component.VJournal -import net.fortuna.ical4j.model.component.VToDo -import net.fortuna.ical4j.model.parameter.AltRep -import net.fortuna.ical4j.model.parameter.Cn -import net.fortuna.ical4j.model.parameter.CuType -import net.fortuna.ical4j.model.parameter.DelegatedFrom -import net.fortuna.ical4j.model.parameter.DelegatedTo -import net.fortuna.ical4j.model.parameter.Dir -import net.fortuna.ical4j.model.parameter.FmtType -import net.fortuna.ical4j.model.parameter.Language -import net.fortuna.ical4j.model.parameter.Member -import net.fortuna.ical4j.model.parameter.PartStat -import net.fortuna.ical4j.model.parameter.RelType -import net.fortuna.ical4j.model.parameter.Related -import net.fortuna.ical4j.model.parameter.Role -import net.fortuna.ical4j.model.parameter.Rsvp -import net.fortuna.ical4j.model.parameter.SentBy -import net.fortuna.ical4j.model.parameter.Value -import net.fortuna.ical4j.model.parameter.XParameter -import net.fortuna.ical4j.model.property.Action -import net.fortuna.ical4j.model.property.Attach -import net.fortuna.ical4j.model.property.Categories -import net.fortuna.ical4j.model.property.Clazz -import net.fortuna.ical4j.model.property.Color -import net.fortuna.ical4j.model.property.Comment -import net.fortuna.ical4j.model.property.Completed -import net.fortuna.ical4j.model.property.Contact -import net.fortuna.ical4j.model.property.Created -import net.fortuna.ical4j.model.property.Description -import net.fortuna.ical4j.model.property.DtEnd -import net.fortuna.ical4j.model.property.DtStamp -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.Due -import net.fortuna.ical4j.model.property.Duration -import net.fortuna.ical4j.model.property.ExDate -import net.fortuna.ical4j.model.property.Geo -import net.fortuna.ical4j.model.property.LastModified -import net.fortuna.ical4j.model.property.Location -import net.fortuna.ical4j.model.property.PercentComplete -import net.fortuna.ical4j.model.property.Priority import net.fortuna.ical4j.model.property.ProdId -import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.RecurrenceId -import net.fortuna.ical4j.model.property.RelatedTo -import net.fortuna.ical4j.model.property.Repeat -import net.fortuna.ical4j.model.property.Resources -import net.fortuna.ical4j.model.property.Sequence -import net.fortuna.ical4j.model.property.Status -import net.fortuna.ical4j.model.property.Summary -import net.fortuna.ical4j.model.property.Trigger -import net.fortuna.ical4j.model.property.Uid -import net.fortuna.ical4j.model.property.Url -import net.fortuna.ical4j.model.property.Version -import net.fortuna.ical4j.model.property.XProperty -import java.io.FileNotFoundException import java.io.IOException import java.io.OutputStream import java.io.Reader -import java.net.URI -import java.net.URISyntaxException -import java.time.format.DateTimeParseException -import java.util.TimeZone import java.util.UUID -import java.util.logging.Level import java.util.logging.Logger open class JtxICalObject( @@ -296,6 +222,8 @@ open class JtxICalObject( val iCalObjectList = mutableListOf() + TODO("ical4j 4.x") + /* ical.components.forEach { component -> val iCalObject = JtxICalObject(collection) @@ -317,7 +245,7 @@ open class JtxICalObject( iCalObjectList.add(iCalObject) } } - } + }*/ return iCalObjectList } @@ -328,8 +256,8 @@ open class JtxICalObject( * @param [calComponents] from which the VAlarms should be extracted */ private fun extractVAlarms(iCalObject: JtxICalObject, calComponents: ComponentList<*>) { - - calComponents.forEach { component -> + TODO("ical4j 4.x") + /*calComponents.forEach { component -> if(component is VAlarm) { val jtxAlarm = Alarm().apply { component.action?.value?.let { vAlarmAction -> this.action = vAlarmAction } @@ -364,7 +292,7 @@ open class JtxICalObject( } iCalObject.alarms.add(jtxAlarm) } - } + }*/ } /** @@ -372,12 +300,12 @@ open class JtxICalObject( * @param [iCalObject] where the properties should be mapped to * @param [properties] from which the properties can be extracted */ - private fun extractProperties(iCalObject: JtxICalObject, properties: PropertyList<*>) { - + private fun extractProperties(iCalObject: JtxICalObject, properties: PropertyList) { // sequence must only be null for locally created, not-yet-synchronized events iCalObject.sequence = 0 - for (prop in properties) { + TODO("ical4j 4.x") + /*for (prop in properties) { when (prop) { is Sequence -> iCalObject.sequence = prop.sequenceNo.toLong() is Created -> iCalObject.created = prop.dateTime.time @@ -625,7 +553,7 @@ open class JtxICalObject( if (iCalObject.duration != null && iCalObject.dtstart == null) { logger.warning("Found DURATION without DTSTART; ignoring") iCalObject.duration = null - } + }*/ } } @@ -638,7 +566,8 @@ open class JtxICalObject( */ fun getICalendarFormat(prodId: ProdId): Calendar? { val ical = Calendar() - ical.properties += Version.VERSION_2_0 + TODO("ical4j 4.x") + /*ical.properties += Version.VERSION_2_0 ical.properties += prodId.withUserAgents(listOf(TaskProvider.ProviderName.JtxBoard.packageName)) val calComponent = when (component) { @@ -730,7 +659,7 @@ open class JtxICalObject( recurInstance.addProperties(recurCalComponent.properties) } - ICalendar.softValidate(ical) + ICalendar.softValidate(ical)*/ return ical } @@ -748,9 +677,9 @@ open class JtxICalObject( * This function maps the current JtxICalObject to a iCalendar property list * @param [props] The PropertyList where the properties should be added */ - private fun addProperties(props: PropertyList) { - - uid.let { props += Uid(it) } + private fun addProperties(props: PropertyList) { + TODO("ical4j 4.x") + /*uid.let { props += Uid(it) } sequence.let { props += Sequence(it.toInt()) } created.let { props += Created(DateTime(it).apply { @@ -1134,8 +1063,6 @@ duration?.let(props::add) } } - /* - // determine earliest referenced date val earliest = arrayOf( dtStart?.date, @@ -1145,7 +1072,8 @@ duration?.let(props::add) // add VTIMEZONE components for (tz in usedTimeZones) ical.components += ICalendar.minifyVTimeZone(tz.vTimeZone, earliest) -*/ + + TODO */ } @@ -1175,7 +1103,6 @@ duration?.let(props::add) * @param [flags] to be set as [Int] */ fun updateFlags(flags: Int) { - var updateUri = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) updateUri = Uri.withAppendedPath(updateUri, this.id.toString()) @@ -1190,7 +1117,8 @@ duration?.let(props::add) * @return the Content [Uri] of the inserted object */ fun add(): Uri { - val values = this.toContentValues() + TODO("ical4j 4.x") + /*val values = this.toContentValues() val newUri = collection.client.insert( JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account), @@ -1200,7 +1128,7 @@ duration?.let(props::add) insertOrUpdateListProperties(false) - return newUri + return newUri*/ } /** @@ -1209,8 +1137,8 @@ duration?.let(props::add) * @return [Uri] of the updated entry */ fun update(data: JtxICalObject): Uri { - - this.applyNewData(data) + TODO("ical4j 4.x") + /*this.applyNewData(data) val values = this.toContentValues() var updateUri = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) @@ -1224,7 +1152,7 @@ duration?.let(props::add) insertOrUpdateListProperties(true) - return updateUri + return updateUri*/ } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 381d3aa7..900c51a3 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -7,7 +7,6 @@ package at.bitfire.ical4android import androidx.annotation.IntRange -import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.property.Clazz @@ -44,22 +43,22 @@ data class Task( var organizer: Organizer? = null, @IntRange(from = 0, to = 9) - var priority: Int = Priority.UNDEFINED.level, + var priority: Int = Priority.VALUE_UNDEFINED, var classification: Clazz? = null, var status: Status? = null, - var dtStart: DtStart? = null, - var due: Due? = null, + var dtStart: DtStart<*>? = null, + var due: Due<*>? = null, var duration: Duration? = null, var completedAt: Completed? = null, @IntRange(from = 0, to = 100) var percentComplete: Int? = null, - var rRule: RRule? = null, - val rDates: LinkedList = LinkedList(), - val exDates: LinkedList = LinkedList(), + var rRule: RRule<*>? = null, + val rDates: LinkedList> = LinkedList(), + val exDates: LinkedList> = LinkedList(), val categories: LinkedList = LinkedList(), var comment: String? = null, @@ -70,9 +69,10 @@ data class Task( ) : ICalendar() { fun isAllDay(): Boolean { - return dtStart?.let { DateUtils.isDate(it) } + TODO("ical4j 4.x") + /*return dtStart?.let { DateUtils.isDate(it) } ?: due?.let { DateUtils.isDate(it) } - ?: true + ?: true*/ } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt index c8c2685e..64661c3d 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskReader.kt @@ -7,39 +7,9 @@ package at.bitfire.ical4android import at.bitfire.ical4android.ICalendar.Companion.fromReader -import at.bitfire.ical4android.util.DateUtils import at.bitfire.synctools.exception.InvalidICalendarException -import at.bitfire.synctools.icalendar.Css3Color import net.fortuna.ical4j.model.Component -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.component.VToDo -import net.fortuna.ical4j.model.property.Categories -import net.fortuna.ical4j.model.property.Clazz -import net.fortuna.ical4j.model.property.Color -import net.fortuna.ical4j.model.property.Comment -import net.fortuna.ical4j.model.property.Completed -import net.fortuna.ical4j.model.property.Created -import net.fortuna.ical4j.model.property.Description -import net.fortuna.ical4j.model.property.DtStamp -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.Due -import net.fortuna.ical4j.model.property.Duration -import net.fortuna.ical4j.model.property.ExDate -import net.fortuna.ical4j.model.property.Geo -import net.fortuna.ical4j.model.property.LastModified -import net.fortuna.ical4j.model.property.Location -import net.fortuna.ical4j.model.property.Organizer -import net.fortuna.ical4j.model.property.PercentComplete -import net.fortuna.ical4j.model.property.Priority -import net.fortuna.ical4j.model.property.ProdId -import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.RelatedTo -import net.fortuna.ical4j.model.property.Sequence -import net.fortuna.ical4j.model.property.Status -import net.fortuna.ical4j.model.property.Summary -import net.fortuna.ical4j.model.property.Uid -import net.fortuna.ical4j.model.property.Url import java.io.IOException import java.io.Reader import java.util.LinkedList @@ -71,7 +41,8 @@ class TaskReader { } private fun fromVToDo(todo: VToDo): Task { - val t = Task() + TODO("ical4j 4.x") + /*val t = Task() if (todo.uid != null) t.uid = todo.uid.value @@ -142,7 +113,7 @@ class TaskReader { t.duration = null } - return t + return t*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt b/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt index 876f3312..cc6226e1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/TaskWriter.kt @@ -6,35 +6,8 @@ package at.bitfire.ical4android -import at.bitfire.ical4android.ICalendar.Companion.minifyVTimeZone -import at.bitfire.ical4android.ICalendar.Companion.softValidate -import at.bitfire.ical4android.ICalendar.Companion.withUserAgents -import at.bitfire.synctools.icalendar.Css3Color -import net.fortuna.ical4j.data.CalendarOutputter -import net.fortuna.ical4j.model.Calendar -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.TextList -import net.fortuna.ical4j.model.TimeZone -import net.fortuna.ical4j.model.component.VToDo -import net.fortuna.ical4j.model.property.Categories -import net.fortuna.ical4j.model.property.Color -import net.fortuna.ical4j.model.property.Comment -import net.fortuna.ical4j.model.property.Created -import net.fortuna.ical4j.model.property.Description -import net.fortuna.ical4j.model.property.LastModified -import net.fortuna.ical4j.model.property.Location -import net.fortuna.ical4j.model.property.PercentComplete -import net.fortuna.ical4j.model.property.Priority import net.fortuna.ical4j.model.property.ProdId -import net.fortuna.ical4j.model.property.Sequence -import net.fortuna.ical4j.model.property.Summary -import net.fortuna.ical4j.model.property.Uid -import net.fortuna.ical4j.model.property.Url -import net.fortuna.ical4j.model.property.Version import java.io.Writer -import java.net.URI -import java.net.URISyntaxException -import java.util.logging.Level import java.util.logging.Logger /** @@ -58,7 +31,8 @@ class TaskWriter( * @param to stream that the iCalendar is written to */ fun write(task: Task, to: Writer): Unit = with(task) { - val ical = Calendar() + TODO() + /*val ical = Calendar() ical.properties += Version.VERSION_2_0 ical.properties += prodId.withUserAgents(userAgents) @@ -135,7 +109,7 @@ class TaskWriter( ical.components += minifyVTimeZone(tz.vTimeZone, earliest) softValidate(ical) - CalendarOutputter(false).output(ical, to) + CalendarOutputter(false).output(ical, to)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt b/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt index 504cd92b..66bcaa6e 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt @@ -16,7 +16,6 @@ import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyBuilder import net.fortuna.ical4j.model.PropertyFactory import org.json.JSONArray -import org.json.JSONObject /** * Helpers to (de)serialize unknown properties as JSON to store it in an Android ExtendedProperty row. @@ -82,12 +81,13 @@ object UnknownProperty { json.put(prop.name) json.put(prop.value) - if (!prop.parameters.isEmpty) { + TODO("ical4j 4.x") + /*if (!prop.parameters.isEmpty) { val jsonParams = JSONObject() for (param in prop.parameters) jsonParams.put(param.name, param.value) json.put(jsonParams) - } + }*/ return json.toString() } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index e7629195..34ad0449 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -7,8 +7,6 @@ package at.bitfire.ical4android.util import net.fortuna.ical4j.data.CalendarBuilder -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.DateProperty @@ -90,14 +88,18 @@ object DateUtils { * @param date date property to check * @return *true* if the date is a DATE value; *false* otherwise (for instance, when the argument is a DATE-TIME value or null) */ - fun isDate(date: DateProperty?) = date != null && date.date is Date && date.date !is DateTime + fun isDate(date: DateProperty<*>?): Boolean = + TODO("ical4j 4.x") + //date != null && date.date is Date && date.date !is DateTime /** * Determines whether a given date represents a DATE-TIME value. * @param date date property to check * @return *true* if the date is a DATE-TIME value; *false* otherwise (for instance, when the argument is a DATE value or null) */ - fun isDateTime(date: DateProperty?) = date != null && date.date is DateTime + fun isDateTime(date: DateProperty<*>?): Boolean = + TODO("ical4j 4.x") + //date != null && date.date is DateTime /** * Parses an iCalendar that only contains a `VTIMEZONE` definition to a VTimeZone object. @@ -110,7 +112,8 @@ object DateUtils { val builder = CalendarBuilder() try { val cal = builder.build(StringReader(timezoneDef)) - return cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone + return TODO("ical4j 4.x") + //return cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone } catch (_: Exception) { // Couldn't parse timezone definition return null diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt index 2cd96283..4a9e10a6 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt @@ -23,9 +23,11 @@ class CalendarUidSplitter { // get all components of type T (for instance: all VEVENTs) val all = calendar.getComponents(componentName) + TODO("ical4j 4.x") + // Note for VEVENT: UID is REQUIRED in RFC 5545 section 3.6.1, but optional in RFC 2445 section 4.6.1, // so it's possible that the Uid is null. - val byUid: Map> = all + /*val byUid: Map> = all .groupBy { it.uid?.value } .mapValues { filterBySequence(it.value) } @@ -36,7 +38,7 @@ class CalendarUidSplitter { result[uid] = AssociatedComponents(mainVEvent, exceptions) } - return result + return result*/ } /** @@ -48,15 +50,17 @@ class CalendarUidSplitter { */ @VisibleForTesting internal fun filterBySequence(events: List): List { + TODO("ical4j 4.x") + // group by RECURRENCE-ID (may be null) - val byRecurId = events.groupBy { it.recurrenceId?.value }.values + /*val byRecurId = events.groupBy { it.recurrenceId?.value }.values // for every RECURRENCE-ID: keep only event with highest sequence val latest = byRecurId.map { sameUidAndRecurId -> sameUidAndRecurId.maxBy { it.sequence?.sequenceNo ?: 0 } } - return latest + return latest*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt index b254d4b3..c928ae42 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt @@ -34,7 +34,8 @@ class ICalendarGenerator { * @param to stream that the iCalendar is written to */ fun write(event: AssociatedComponents<*>, @WillNotClose to: Writer) { - val ical = Calendar() + TODO("ical4j 4.x") + /*val ical = Calendar() ical.properties += Version.VERSION_2_0 // add PRODID @@ -68,11 +69,12 @@ class ICalendarGenerator { for (tz in usedTimeZones) ical.components += ICalendar.minifyVTimeZone(tz.vTimeZone, earliestStart) - CalendarOutputter(false).output(ical, to) + CalendarOutputter(false).output(ical, to)*/ } private fun timeZonesOf(component: CalendarComponent): Set { - val timeZones = mutableSetOf() + TODO("ical4j 4.x") + /*val timeZones = mutableSetOf() // properties timeZones += component.properties @@ -86,7 +88,7 @@ class ICalendarGenerator { .filterIsInstance() .mapNotNull { (it.date as? DateTime)?.timeZone } - return timeZones + return timeZones*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt index c1a0dc47..f2bdd8de 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt @@ -42,8 +42,10 @@ class ICalendarParser( * @throws InvalidICalendarException when the resource is can't be parsed */ fun parse(@WillNotClose reader: Reader): Calendar { + TODO("ical4j 4.x") + // preprocess stream to work around problems that prevent parsing and thus can't be fixed later - val preprocessed = preprocessor.preprocessStream(reader) + /*val preprocessed = preprocessor.preprocessStream(reader) // parse stream, ignoring invalid properties (if possible) val calendar: Calendar @@ -66,7 +68,7 @@ class ICalendarParser( logger.log(Level.WARNING, "Couldn't pre-process iCalendar", e) } - return calendar + return calendar*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt index d57b65a7..50840fdc 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt @@ -27,24 +27,30 @@ const val ical4jVersion = BuildConfig.version_ical4j // component access helpers -fun componentListOf(vararg components: T) = - ComponentList().apply { +fun componentListOf(vararg components: T): ComponentList = + TODO("ical4j 4.x") + /*ComponentList().apply { addAll(components) - } + }*/ -fun propertyListOf(vararg properties: Property) = - PropertyList().apply { +fun propertyListOf(vararg properties: Property): PropertyList = + TODO("ical4j 4.x") + /*PropertyList().apply { addAll(properties) - } + }*/ val CalendarComponent.uid: Uid? - get() = getProperty(Property.UID) + get() = TODO("ical4j 4.x") + // getProperty(Property.UID) -val CalendarComponent.recurrenceId: RecurrenceId? - get() = getProperty(Property.RECURRENCE_ID) +val CalendarComponent.recurrenceId: RecurrenceId<*>? + get() = TODO("ical4j 4.x") + // getProperty(Property.RECURRENCE_ID) val CalendarComponent.sequence: Sequence? - get() = getProperty(Property.SEQUENCE) + get() = TODO("ical4j 4.x") + // getProperty(Property.SEQUENCE) -fun VEvent.requireDtStart(): DtStart = - startDate ?: throw InvalidICalendarException("Missing DTSTART in VEVENT") +fun VEvent.requireDtStart(): DtStart<*> = + TODO("ical4j 4.x") + // startDate ?: throw InvalidICalendarException("Missing DTSTART in VEVENT") diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt index baeb9cdd..1f7fc029 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt @@ -10,10 +10,8 @@ import androidx.annotation.VisibleForTesting import com.google.common.io.CharSource import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.transform.rfc5545.CreatedPropertyRule -import net.fortuna.ical4j.transform.rfc5545.DateListPropertyRule -import net.fortuna.ical4j.transform.rfc5545.DatePropertyRule -import net.fortuna.ical4j.transform.rfc5545.Rfc5545PropertyRule +import net.fortuna.ical4j.transform.compliance.DateListPropertyRule +import net.fortuna.ical4j.transform.compliance.DatePropertyRule import java.io.BufferedReader import java.io.Reader import java.util.logging.Logger @@ -32,7 +30,8 @@ class ICalPreprocessor { get() = Logger.getLogger(javaClass.name) private val propertyRules = arrayOf( - CreatedPropertyRule(), // make sure CREATED is UTC + TODO("ical4j 4.x"), + //CreatedPropertyRule(), // make sure CREATED is UTC DatePropertyRule(), // These two rules also replace VTIMEZONEs of the iCalendar ... DateListPropertyRule() // ... by the ical4j VTIMEZONE with the same TZID! @@ -97,14 +96,16 @@ class ICalPreprocessor { * @param calendar the calendar object that is going to be modified */ fun preprocessCalendar(calendar: Calendar) { - for (component in calendar.components) + TODO("ical4j 4.x") + /*for (component in calendar.components) for (property in component.properties) - applyRules(property) + applyRules(property)*/ } @Suppress("UNCHECKED_CAST") private fun applyRules(property: Property) { - propertyRules + TODO("ical4j 4.x") + /*propertyRules .filter { rule -> rule.supportedType.isAssignableFrom(property::class.java) } .forEach { rule -> val beforeStr = property.toString() @@ -112,7 +113,7 @@ class ICalPreprocessor { val afterStr = property.toString() if (beforeStr != afterStr) logger.info("${rule.javaClass.name}: $beforeStr -> $afterStr") - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt index 6889277d..389bda99 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt @@ -10,6 +10,7 @@ import android.content.Entity import android.provider.CalendarContract.Events import android.provider.CalendarContract.ExtendedProperties import at.bitfire.synctools.icalendar.AssociatedEvents +import at.bitfire.synctools.icalendar.recurrenceId import at.bitfire.synctools.mapping.calendar.handler.AccessLevelHandler import at.bitfire.synctools.mapping.calendar.handler.AndroidEventFieldHandler import at.bitfire.synctools.mapping.calendar.handler.AttendeesHandler @@ -121,8 +122,8 @@ class AndroidEventHandler( ) // add exceptions of recurring main event - val rRules = main.getProperties(Property.RRULE) - val rDates = main.getProperties(Property.RDATE) + val rRules = main.getProperties>(Property.RRULE) + val rDates = main.getProperties>(Property.RDATE) val exceptions = LinkedList() if (rRules.isNotEmpty() || rDates.isNotEmpty()) { for (exception in eventAndExceptions.exceptions) { @@ -136,10 +137,11 @@ class AndroidEventHandler( val recurrenceId = exceptionEvent.recurrenceId ?: continue // generate EXDATE instead of VEVENT with RECURRENCE-ID for cancelled instances - if (exception.entityValues.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED) + TODO("ical4j 4.x") + /*if (exception.entityValues.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED) main.properties += asExDate(exception, recurrenceId) else - exceptions += exceptionEvent + exceptions += exceptionEvent*/ } } @@ -155,8 +157,9 @@ class AndroidEventHandler( ) } - private fun asExDate(entity: Entity, recurrenceId: RecurrenceId): ExDate { - val originalAllDay = (entity.entityValues.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0 + private fun asExDate(entity: Entity, recurrenceId: RecurrenceId<*>): ExDate<*> { + TODO("ical4j 4.x") + /*val originalAllDay = (entity.entityValues.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0 val list = DateList( if (originalAllDay) Value.DATE else Value.DATE_TIME, recurrenceId.timeZone @@ -170,7 +173,7 @@ class AndroidEventHandler( else timeZone = recurrenceId.timeZone } - } + }*/ } private fun generateProdId(main: Entity): ProdId { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt index 1a8be62f..a9dbeeee 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt @@ -79,10 +79,11 @@ object AttendeeMappings { } - if (cuType != null && cuType != CuType.INDIVIDUAL) + TODO("ical4j 4.x") + /*if (cuType != null && cuType != CuType.INDIVIDUAL) attendee.parameters.add(cuType) if (role != null && role != Role.REQ_PARTICIPANT) - attendee.parameters.add(role) + attendee.parameters.add(role)*/ } @@ -112,8 +113,10 @@ object AttendeeMappings { val type: Int var relationship: Int - val cuType = attendee.getParameter(Parameter.CUTYPE) ?: CuType.INDIVIDUAL - val role = attendee.getParameter(Parameter.ROLE) ?: Role.REQ_PARTICIPANT + val cuType: CuType = TODO("ical4j 4.x") + // attendee.getParameter(Parameter.CUTYPE) ?: CuType.INDIVIDUAL + val role: Role = TODO("ical4j 4.x") + // attendee.getParameter(Parameter.ROLE) ?: Role.REQ_PARTICIPANT when (cuType) { CuType.RESOURCE -> { @@ -160,7 +163,8 @@ object AttendeeMappings { val email = if (uri.scheme.equals("mailto", true)) uri.schemeSpecificPart else - attendee.getParameter(Parameter.EMAIL)?.value + TODO("ical4j 4.x") + //attendee.getParameter(Parameter.EMAIL)?.value if (email == organizer) relationship = Attendees.RELATIONSHIP_ORGANIZER diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt index eedc9e85..9fa344d0 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt @@ -20,7 +20,8 @@ class AccessLevelBuilder: AndroidEntityBuilder { val accessLevel: Int val retainValue: Boolean - val classification = from.classification + TODO("ical4j 4.x") + /*val classification = from.classification when (classification) { Clazz.PUBLIC -> { accessLevel = Events.ACCESS_PUBLIC @@ -60,6 +61,7 @@ class AccessLevelBuilder: AndroidEntityBuilder { ExtendedProperties.VALUE to UnknownProperty.toJsonString(classification) ) ) + */ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt index 80b79017..e56d2821 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt @@ -14,8 +14,9 @@ import net.fortuna.ical4j.model.component.VEvent class AllDayBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - val allDay = DateUtils.isDate(from.startDate) - to.entityValues.put(Events.ALL_DAY, if (allDay) 1 else 0) + TODO("ical4j 4.x") + /*val allDay = DateUtils.isDate(from.startDate) + to.entityValues.put(Events.ALL_DAY, if (allDay) 1 else 0)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt index 27faa9e2..9c93a13d 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt @@ -36,7 +36,8 @@ class AttendeesBuilder( calendar.ownerAccount ?: calendar.account.name val member = attendee.calAddress - if (member.scheme.equals("mailto", true)) // attendee identified by email + TODO("ical4j 4.x") + /*if (member.scheme.equals("mailto", true)) // attendee identified by email values.put(Attendees.ATTENDEE_EMAIL, member.schemeSpecificPart) else { // attendee identified by other URI @@ -62,7 +63,7 @@ class AttendeesBuilder( PartStat.DELEGATED -> Attendees.ATTENDEE_STATUS_NONE else /* default: PartStat.NEEDS_ACTION */ -> Attendees.ATTENDEE_STATUS_INVITED } - values.put(Attendees.ATTENDEE_STATUS, status) + values.put(Attendees.ATTENDEE_STATUS, status)*/ return values } @@ -74,7 +75,8 @@ class AttendeesBuilder( return if (uri.scheme.equals("mailto", true)) uri.schemeSpecificPart else - organizer.getParameter(Parameter.EMAIL)?.value + TODO("ical4j 4.x") + // organizer.getParameter(Parameter.EMAIL)?.value } return null } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt index b085ec51..931d772f 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt @@ -14,7 +14,8 @@ import net.fortuna.ical4j.model.property.Transp class AvailabilityBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - val availability = when (from.transparency) { + TODO("ical4j 4.x") + /*val availability = when (from.transparency) { Transp.TRANSPARENT -> Events.AVAILABILITY_FREE @@ -25,7 +26,7 @@ class AvailabilityBuilder: AndroidEntityBuilder { to.entityValues.put( Events.AVAILABILITY, availability - ) + )*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt index b5fa60a7..3468ae3a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt @@ -17,7 +17,8 @@ import net.fortuna.ical4j.model.property.Categories class CategoriesBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - val categories = from.getProperty(Property.CATEGORIES)?.categories + TODO("ical4j 4.x") + /*val categories = from.getProperty(Property.CATEGORIES)?.categories if (categories != null && !categories.isEmpty) { val rawCategories = categories.joinToString(EventsContract.CATEGORIES_SEPARATOR.toString()) { category -> // drop occurrences of CATEGORIES_SEPARATOR in category names @@ -31,7 +32,7 @@ class CategoriesBuilder: AndroidEntityBuilder { ExtendedProperties.VALUE to rawCategories ) ) - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt index 753adb36..08264452 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt @@ -22,7 +22,8 @@ class ColorBuilder( override fun build(from: VEvent, main: VEvent, to: Entity) { val values = to.entityValues - val color = from.getProperty(Color.PROPERTY_NAME)?.value + TODO("ical4j 4.x") + /*val color = from.getProperty(Color.PROPERTY_NAME)?.value if (color != null && hasColor(color)) { // set event color (if it's available for this account) values.put(Events.EVENT_COLOR_KEY, color) @@ -30,7 +31,7 @@ class ColorBuilder( // reset color index and value values.putNull(Events.EVENT_COLOR_KEY) values.putNull(Events.EVENT_COLOR) - } + }*/ } @VisibleForTesting diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt index ff48cea4..41a209b5 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilder.kt @@ -35,7 +35,8 @@ class DurationBuilder: AndroidEntityBuilder { - DURATION when the event is recurring. So we'll skip if this event is not a recurring main event (only main events can be recurring). */ - val rRules = from.getProperties(Property.RRULE) + TODO("ical4j 4.x") + /*val rRules = from.getProperties(Property.RRULE) val rDates = from.getProperties(Property.RDATE) if (from !== main || (rRules.isEmpty() && rDates.isEmpty())) { values.putNull(Events.DURATION) @@ -68,7 +69,7 @@ class DurationBuilder: AndroidEntityBuilder { The calendar provider accepts every DURATION that `com.android.calendarcommon2.Duration` can parse, which is weeks, days, hours, minutes and seconds, like for the RFC 5545 duration. */ val durationStr = alignedDuration.toRfc5545Duration(dtStart.date.toInstant()) - values.put(Events.DURATION, durationStr) + values.put(Events.DURATION, durationStr)*/ } /** @@ -83,8 +84,9 @@ class DurationBuilder: AndroidEntityBuilder { * - a [Duration] (exact time that can be represented by an exact number of seconds) when [dtStart] is a DATE-TIME. */ @VisibleForTesting - internal fun alignWithDtStart(amount: TemporalAmount, dtStart: DtStart): TemporalAmount { - if (DateUtils.isDate(dtStart)) { + internal fun alignWithDtStart(amount: TemporalAmount, dtStart: DtStart<*>): TemporalAmount { + TODO("ical4j 4.x") + /*if (DateUtils.isDate(dtStart)) { // DTSTART is DATE return if (amount is Duration) { // amount is Duration, change to Period of days instead @@ -103,7 +105,7 @@ class DurationBuilder: AndroidEntityBuilder { // amount is already Duration amount } - } + }*/ } /** @@ -115,8 +117,9 @@ class DurationBuilder: AndroidEntityBuilder { * @return temporal amount ([Period] or [Duration]) or `null` if no valid end time was available */ @VisibleForTesting - internal fun calculateFromDtEnd(dtStart: DtStart, dtEnd: DtEnd?): TemporalAmount? { - if (dtEnd == null || dtEnd.date.toInstant() <= dtStart.date.toInstant()) + internal fun calculateFromDtEnd(dtStart: DtStart<*>, dtEnd: DtEnd<*>?): TemporalAmount? { + TODO("ical4j 4.x") + /*if (dtEnd == null || dtEnd.date.toInstant() <= dtStart.date.toInstant()) return null return if (DateUtils.isDateTime(dtStart) && DateUtils.isDateTime(dtEnd)) { @@ -131,7 +134,7 @@ class DurationBuilder: AndroidEntityBuilder { val startDate = dtStart.date.toLocalDate() val endDate = dtEnd.date.toLocalDate() Period.between(startDate, endDate) - } + }*/ } private fun defaultDuration(allDay: Boolean): TemporalAmount = diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt index 8e071a87..aaf65405 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt @@ -41,7 +41,8 @@ class EndTimeBuilder: AndroidEntityBuilder { - DURATION when the event is recurring. So we'll skip if this event is a recurring main event (only main events can be recurring). */ - val rRules = from.getProperties(Property.RRULE) + TODO("ical4j 4.x") + /*val rRules = from.getProperties(Property.RRULE) val rDates = from.getProperties(Property.RDATE) if (from === main && (rRules.isNotEmpty() || rDates.isNotEmpty())) { values.putNull(Events.DTEND) @@ -91,7 +92,7 @@ class EndTimeBuilder: AndroidEntityBuilder { } else { // DTEND is a DATE values.put(Events.EVENT_END_TIMEZONE, AndroidTimeUtils.TZID_UTC) - } + }*/ } @@ -110,8 +111,9 @@ class EndTimeBuilder: AndroidEntityBuilder { * @see at.bitfire.synctools.mapping.calendar.handler.RecurrenceFieldsHandler.alignUntil */ @VisibleForTesting - internal fun alignWithDtStart(dtEnd: DtEnd, dtStart: DtStart): DtEnd { - if (DateUtils.isDate(dtEnd)) { + internal fun alignWithDtStart(dtEnd: DtEnd<*>, dtStart: DtStart<*>): DtEnd<*> { + TODO("ical4j 4.x") + /*if (DateUtils.isDate(dtEnd)) { // DTEND is DATE if (DateUtils.isDate(dtStart)) { // DTEND is DATE, DTSTART is DATE @@ -133,7 +135,7 @@ class EndTimeBuilder: AndroidEntityBuilder { // DTEND is DATE-TIME, DTSTART is DATE-TIME return dtEnd } - } + }*/ } /** @@ -145,13 +147,14 @@ class EndTimeBuilder: AndroidEntityBuilder { * @return end date/date-time (same value type as [dtStart]) or `null` if [duration] was not given */ @VisibleForTesting - internal fun calculateFromDuration(dtStart: DtStart, duration: TemporalAmount?): DtEnd? { + internal fun calculateFromDuration(dtStart: DtStart<*>, duration: TemporalAmount?): DtEnd<*>? { if (duration == null) return null val dur = duration.abs() // always take positive temporal amount - return if (DateUtils.isDate(dtStart)) { + TODO("ical4j 4.x") + /*return if (DateUtils.isDate(dtStart)) { // DTSTART is DATE if (dur is Period) { // date-based amount of time ("4 days") @@ -170,7 +173,7 @@ class EndTimeBuilder: AndroidEntityBuilder { // We can add both date-based (Period) and time-based (Duration) amounts of time to an exact date/time. val result = (dtStart.date as DateTime).toZonedDateTime() + dur DtEnd(result.toIcal4jDateTime()) - } + }*/ } /** @@ -194,14 +197,15 @@ class EndTimeBuilder: AndroidEntityBuilder { * - when [dtStart] is a `DATE-TIME`: [dtStart] */ @VisibleForTesting - internal fun calculateFromDefault(dtStart: DtStart): DtEnd = - if (DateUtils.isDate(dtStart)) { + internal fun calculateFromDefault(dtStart: DtStart<*>): DtEnd<*> = + TODO("ical4j 4.x") + /*if (DateUtils.isDate(dtStart)) { // DATE → one day duration val endDate: LocalDate = dtStart.date.toLocalDate().plusDays(1) DtEnd(endDate.toIcal4jDate()) } else { // DATE-TIME → same as DTSTART to indicate there was no DTEND set DtEnd(dtStart.value, dtStart.timeZone) - } + }*/ } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt index 368f64f0..4789c2b1 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt @@ -46,7 +46,8 @@ class OrganizerBuilder( return null // Take from mailto: value or EMAIL parameter - val uri: URI? = organizer.calAddress + TODO("ical4j 4.x") + /*val uri: URI? = organizer.calAddress val email = if (uri?.scheme.equals("mailto", true)) uri?.schemeSpecificPart else @@ -55,7 +56,7 @@ class OrganizerBuilder( if (email != null) return email - logger.log(Level.WARNING, "Ignoring ORGANIZER without email address (not supported by Android)", organizer) + logger.log(Level.WARNING, "Ignoring ORGANIZER without email address (not supported by Android)", organizer)*/ return null } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt index b9403480..8c91d633 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilder.kt @@ -14,6 +14,7 @@ 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.toLocalTime +import at.bitfire.synctools.icalendar.recurrenceId import at.bitfire.synctools.icalendar.requireDtStart import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime @@ -23,7 +24,9 @@ import java.time.ZonedDateTime class OriginalInstanceTimeBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - val values = to.entityValues + TODO("ical4j 4.x") + + /*val values = to.entityValues if (from !== main) { // only for exceptions val originalDtStart = main.requireDtStart() @@ -55,7 +58,7 @@ class OriginalInstanceTimeBuilder: AndroidEntityBuilder { // main event values.putNull(Events.ORIGINAL_ALL_DAY) values.putNull(Events.ORIGINAL_INSTANCE_TIME) - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilder.kt index cd61a40d..e0020a5b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilder.kt @@ -27,7 +27,8 @@ class RecurrenceFieldsBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { val values = to.entityValues - val rRules = from.getProperties(Property.RRULE) + TODO("ical4j 4.x") + /*val rRules = from.getProperties(Property.RRULE) val rDates = from.getProperties(Property.RDATE) val recurring = rRules.isNotEmpty() || rDates.isNotEmpty() if (recurring && from === main) { @@ -84,7 +85,7 @@ class RecurrenceFieldsBuilder: AndroidEntityBuilder { values.putNull(Events.EXRULE) values.putNull(Events.RDATE) values.putNull(Events.EXDATE) - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt index ac595091..c545f4cf 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt @@ -24,7 +24,8 @@ class RemindersBuilder: AndroidEntityBuilder { } private fun buildReminder(alarm: VAlarm, event: VEvent): ContentValues { - val method = when (alarm.action?.value?.uppercase(Locale.ROOT)) { + TODO("ical4j 4.x") + /*val method = when (alarm.action?.value?.uppercase(Locale.ROOT)) { Action.DISPLAY.value, Action.AUDIO.value -> Reminders.METHOD_ALERT // will trigger an alarm on the Android device @@ -45,7 +46,7 @@ class RemindersBuilder: AndroidEntityBuilder { return contentValuesOf( Reminders.METHOD to method, Reminders.MINUTES to minutes - ) + )*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt index ad5d4bae..082d07b9 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt @@ -17,7 +17,8 @@ import java.time.ZoneId class StartTimeBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - val values = to.entityValues + TODO("ical4j 4.x") + /*val values = to.entityValues val dtStart = from.requireDtStart() @@ -47,7 +48,7 @@ class StartTimeBuilder: AndroidEntityBuilder { } else { // DTSTART is a DATE values.put(Events.EVENT_TIMEZONE, AndroidTimeUtils.TZID_UTC) - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt index b4490fdf..24a26b69 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt @@ -14,12 +14,13 @@ import net.fortuna.ical4j.model.property.Status class StatusBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - to.entityValues.put(Events.STATUS, when (from.status) { + TODO("ical4j 4.x") + /*to.entityValues.put(Events.STATUS, when (from.status) { Status.VEVENT_CONFIRMED -> Events.STATUS_CONFIRMED Status.VEVENT_CANCELLED -> Events.STATUS_CANCELED null -> null else -> Events.STATUS_TENTATIVE - }) + })*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt index 5c8c7b3d..bbd722bf 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt @@ -15,7 +15,8 @@ class UidBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { // Always take UID from main event because exceptions must have the same UID anyway. // Note: RFC 5545 requires UID for VEVENTs, however the obsoleted RFC 2445 does not. - to.entityValues.put(Events.UID_2445, main.uid?.value) + TODO("ical4j 4.x") + /*to.entityValues.put(Events.UID_2445, main.uid?.value)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt index dcf9e0b3..aad9959b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt @@ -48,9 +48,10 @@ class UnknownPropertiesBuilder: AndroidEntityBuilder { @VisibleForTesting internal fun unknownProperties(event: VEvent): List = - event.properties.filterNot { + TODO("ical4j 4.x") + /*event.properties.filterNot { KNOWN_PROPERTY_NAMES.contains(it.name.uppercase()) - } + }*/ companion object { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt index 290981b6..89d0ad1a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt @@ -20,7 +20,8 @@ class AccessLevelHandler: AndroidEventFieldHandler { val values = from.entityValues // take classification from main row - val classification = when (values.getAsInteger(Events.ACCESS_LEVEL)) { + TODO("ical4j 4.x") + /*val classification = when (values.getAsInteger(Events.ACCESS_LEVEL)) { Events.ACCESS_PUBLIC -> Clazz.PUBLIC @@ -30,11 +31,11 @@ class AccessLevelHandler: AndroidEventFieldHandler { Events.ACCESS_CONFIDENTIAL -> Clazz.CONFIDENTIAL - else /* Events.ACCESS_DEFAULT */ -> + else *//* Events.ACCESS_DEFAULT *//* -> retainedClassification(from) } if (classification != null) - to.properties += classification + to.properties += classification*/ } private fun retainedClassification(from: Entity): Clazz? { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandler.kt index f3471c04..09f40176 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandler.kt @@ -34,7 +34,8 @@ class AttendeesHandler: AndroidEventFieldHandler { private fun populateAttendee(row: ContentValues, to: VEvent) { logger.log(Level.FINE, "Read event attendee from calendar provider", row) - try { + TODO("ical4j 4.x") + /*try { val attendee: Attendee val email = row.getAsString(Attendees.ATTENDEE_EMAIL) val idNS = row.getAsString(Attendees.ATTENDEE_ID_NAMESPACE) @@ -69,7 +70,7 @@ class AttendeesHandler: AndroidEventFieldHandler { to.properties += attendee } catch (e: URISyntaxException) { logger.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e) - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandler.kt index 69393f09..9e032357 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandler.kt @@ -14,7 +14,8 @@ import net.fortuna.ical4j.model.property.Transp class AvailabilityHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { - val transp: Transp = when (from.entityValues.getAsInteger(Events.AVAILABILITY)) { + TODO("ical4j 4.x") + /*val transp: Transp = when (from.entityValues.getAsInteger(Events.AVAILABILITY)) { Events.AVAILABILITY_FREE -> Transp.TRANSPARENT @@ -23,7 +24,7 @@ class AvailabilityHandler: AndroidEventFieldHandler { Transp.OPAQUE } if (transp != Transp.OPAQUE) // iCalendar default value is OPAQUE - to.properties += transp + to.properties += transp*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandler.kt index d7e17da9..023e65e1 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandler.kt @@ -20,9 +20,10 @@ class CategoriesHandler: AndroidEventFieldHandler { val categories = extended.firstOrNull { it.getAsString(ExtendedProperties.NAME) == EventsContract.EXTNAME_CATEGORIES } val listValue = categories?.getAsString(ExtendedProperties.VALUE) if (listValue != null) { - to.properties += Categories(TextList( + TODO("ical4j 4.x") + /*to.properties += Categories(TextList( listValue.split(EventsContract.CATEGORIES_SEPARATOR).toTypedArray() - )) + ))*/ } } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt index c8efcadd..ac205816 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt @@ -34,8 +34,9 @@ class ColorHandler: AndroidEventFieldHandler { Css3Color.entries.firstOrNull { it.argb == color } } - if (color != null) - to.properties += Color(null, color.name) + TODO("ical4j 4.x") + /*if (color != null) + to.properties += Color(null, color.name)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt index 0aa4f52f..92687131 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt @@ -15,9 +15,10 @@ import net.fortuna.ical4j.model.property.Description class DescriptionHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { - val description = from.entityValues.getAsString(Events.DESCRIPTION).trimToNull() + TODO("ical4j 4.x") + /*val description = from.entityValues.getAsString(Events.DESCRIPTION).trimToNull() if (description != null) - to.properties += Description(description) + to.properties += Description(description)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt index 78cc7069..5c969611 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt @@ -52,7 +52,8 @@ class DurationHandler( val tsStart = values.getAsLong(Events.DTSTART) ?: return val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0 - if (allDay) { + TODO("ical4j 4.x") + /*if (allDay) { val startTimeUTC = Instant.ofEpochMilli(tsStart).atOffset(ZoneOffset.UTC) val endDate = (startTimeUTC + duration).toLocalDate() @@ -72,7 +73,7 @@ class DurationHandler( val end = start + duration to.properties += DtEnd(end.toIcal4jDateTime(tzRegistry)) - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt index adce1a29..01382478 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt @@ -60,7 +60,8 @@ class EndTimeHandler( tzRegistry = tzRegistry ).asIcal4jDate() - to.properties += DtEnd(end) + TODO("ical4j 4.x") + //to.properties += DtEnd(end) } @VisibleForTesting diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt index a86ac604..51c429e2 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt @@ -16,8 +16,9 @@ class LocationHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val location = from.entityValues.getAsString(Events.EVENT_LOCATION).trimToNull() - if (location != null) - to.properties += Location(location) + TODO("ical4j 4.x") + /*if (location != null) + to.properties += Location(location)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandler.kt index b87abd63..0142acf5 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandler.kt @@ -30,7 +30,8 @@ class OrganizerHandler: AndroidEventFieldHandler { val hasAttendees = from.subValues.any { it.uri == Attendees.CONTENT_URI } if (hasAttendees && mainValues.containsKey(Events.ORGANIZER)) try { - to.properties += Organizer(URI("mailto", mainValues.getAsString(Events.ORGANIZER), null)) + TODO("ical4j 4.x") + //to.properties += Organizer(URI("mailto", mainValues.getAsString(Events.ORGANIZER), null)) } catch (e: URISyntaxException) { logger.log(Level.WARNING, "Error when creating ORGANIZER mailto URI, ignoring", e) } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt index ac2120a7..5373a303 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt @@ -26,7 +26,8 @@ class OriginalInstanceTimeHandler( return val values = from.entityValues - values.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime -> + TODO("ical4j 4.x") + /*values.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime -> val originalAllDay = (values.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0 val originalDate = if (originalAllDay) @@ -48,7 +49,7 @@ class OriginalInstanceTimeHandler( } to.properties += RecurrenceId(originalDate) - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt index 7833a8ae..fb55c593 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldsHandler.kt @@ -51,8 +51,11 @@ class RecurrenceFieldsHandler( ).asIcal4jDate() } + TODO("ical4j 4.x") + // Note: big method – maybe split? + // process RRULE field - val rRules = LinkedList() + /*val rRules = LinkedList() values.getAsString(Events.RRULE)?.let { rRuleField -> try { for (rule in rRuleField.split(AndroidTimeUtils.RECURRENCE_RULE_SEPARATOR)) { @@ -130,7 +133,7 @@ class RecurrenceFieldsHandler( to.properties += rDates to.properties += exRules to.properties += exDates - } + }*/ } /** @@ -151,8 +154,9 @@ class RecurrenceFieldsHandler( * * @see at.bitfire.synctools.mapping.calendar.builder.EndTimeBuilder.alignWithDtStart */ - fun alignUntil(recur: Recur, startDate: Date): Recur { - val until: Date = recur.until ?: return recur + fun alignUntil(recur: Recur<*>, startDate: Date): Recur<*> { + TODO("ical4j 4.x") + /*val until: Date = recur.until ?: return recur if (until is DateTime) { // UNTIL is DATE-TIME @@ -182,7 +186,7 @@ class RecurrenceFieldsHandler( // DTSTART is DATE return recur } - } + }*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandler.kt index 729852ce..3be0c41e 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandler.kt @@ -40,7 +40,8 @@ class RemindersHandler( val eventTitle = event.entityValues.getAsString(Events.TITLE) ?: "Calendar Event Reminder" val alarm = VAlarm(Duration.ofMinutes(-row.getAsLong(Reminders.MINUTES))) - val props = alarm.properties + TODO("ical4j 4.x") + /*val props = alarm.properties when (row.getAsInteger(Reminders.METHOD)) { Reminders.METHOD_EMAIL -> { if (Patterns.EMAIL_ADDRESS.matcher(accountName).matches()) { @@ -64,7 +65,7 @@ class RemindersHandler( props += Description(eventTitle) } } - to.components += alarm + to.components += alarm*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandler.kt index af4a0deb..447b040b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandler.kt @@ -15,8 +15,9 @@ class SequenceHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val seqNo = from.entityValues.getAsInteger(EventsContract.COLUMN_SEQUENCE) - if (seqNo != null && seqNo > 0) - to.properties += Sequence(seqNo) + TODO("ical4j 4.x") + /*if (seqNo != null && seqNo > 0) + to.properties += Sequence(seqNo)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt index 2539e78b..e97c75e4 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt @@ -29,7 +29,8 @@ class StartTimeHandler( tzRegistry = tzRegistry ).asIcal4jDate() - to.properties += DtStart(start) + TODO("ical4j 4.x") + //to.properties += DtStart(start) } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandler.kt index 9c8f2730..7eb9a925 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandler.kt @@ -14,7 +14,8 @@ import net.fortuna.ical4j.model.property.Status class StatusHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { - val status = when (from.entityValues.getAsInteger(Events.STATUS)) { + TODO("ical4j 4.x") + /*val status = when (from.entityValues.getAsInteger(Events.STATUS)) { Events.STATUS_CONFIRMED -> Status.VEVENT_CONFIRMED @@ -28,7 +29,7 @@ class StatusHandler: AndroidEventFieldHandler { null } if (status != null) - to.properties += status + to.properties += status*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt index 2894cc90..1b26c592 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt @@ -16,8 +16,9 @@ class TitleHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val summary = from.entityValues.getAsString(Events.TITLE).trimToNull() - if (summary != null) - to.properties += Summary(summary) + TODO("ical4j 4.x") + /*if (summary != null) + to.properties += Summary(summary)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt index 92633d8a..0c2d358a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt @@ -17,8 +17,9 @@ class UidHandler: AndroidEventFieldHandler { // Should always be available because AndroidEventHandler ensures there's a UID to be RFC 5545-compliant. // However technically it can be null (and no UID is OK according to RFC 2445). val uid = main.entityValues.getAsString(Events.UID_2445) - if (uid != null) - to.properties += Uid(uid) + TODO("ical4j 4.x") + /*if (uid != null) + to.properties += Uid(uid)*/ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandler.kt index 1c8994b0..b8fd32e1 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandler.kt @@ -27,8 +27,9 @@ class UnknownPropertiesHandler: AndroidEventFieldHandler { for (json in jsonProperties) try { val prop = UnknownProperty.fromJsonString(json) - if (!EXCLUDED.contains(prop.name)) - to.properties += prop + TODO("ical4j 4.x") + /*if (!EXCLUDED.contains(prop.name)) + to.properties += prop*/ } catch (e: JSONException) { logger.log(Level.WARNING, "Couldn't parse unknown properties", e) } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandler.kt index b12931ab..868379e4 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandler.kt @@ -26,8 +26,9 @@ class UrlHandler: AndroidEventFieldHandler { } catch (_: URISyntaxException) { null } - if (uri != null) - to.properties += Url(uri) + TODO("ical4j 4.x") + /*if (uri != null) + to.properties += Url(uri)*/ } } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt index fab70455..a7ff6096 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt @@ -94,7 +94,10 @@ class DmfsTaskBuilder( .withValue(Tasks.PARENT_ID, null) // organizer - task.organizer?.let { organizer -> + TODO("ical4j 4.x") + // Note: big method – maybe split? Depends on how we want to proceed with refactoring. + + /*task.organizer?.let { organizer -> val uri = organizer.calAddress val email = if (uri.scheme.equals("mailto", true)) uri.schemeSpecificPart @@ -160,11 +163,12 @@ class DmfsTaskBuilder( else AndroidTimeUtils.recurrenceSetsToOpenTasksString(task.exDates, if (allDay) null else getTimeZone())) - logger.log(Level.FINE, "Built task object", builder.build()) + logger.log(Level.FINE, "Built task object", builder.build())*/ } fun getTimeZone(): TimeZone { - return task.dtStart?.let { dtStart -> + TODO("ical4j 4.x") + /*return task.dtStart?.let { dtStart -> if (dtStart.isUtc) tzRegistry.getTimeZone(TimeZones.UTC_ID) else @@ -176,7 +180,7 @@ class DmfsTaskBuilder( else due.timeZone } ?: - tzRegistry.getTimeZone(ZoneId.systemDefault().id)!! + tzRegistry.getTimeZone(ZoneId.systemDefault().id)!!*/ } fun insertProperties(batch: TasksBatchOperation, idxTask: Int?) { @@ -203,7 +207,8 @@ class DmfsTaskBuilder( Alarm.ALARM_REFERENCE_START_DATE } - val alarmType = when (alarm.action?.value?.uppercase(Locale.ROOT)) { + TODO("ical4j 4.x") + /*val alarmType = when (alarm.action?.value?.uppercase(Locale.ROOT)) { Action.AUDIO.value -> Alarm.ALARM_TYPE_SOUND Action.DISPLAY.value -> @@ -224,7 +229,7 @@ class DmfsTaskBuilder( .withValue(Alarm.ALARM_TYPE, alarmType) logger.log(Level.FINE, "Inserting alarm", builder.build()) - batch += builder + batch += builder*/ } } @@ -250,13 +255,14 @@ class DmfsTaskBuilder( } private fun insertRelatedTo(batch: TasksBatchOperation, idxTask: Int?) { - for (relatedTo in task.relatedTo) { + TODO("ical4j 4.x") + /*for (relatedTo in task.relatedTo) { val relType = when ((relatedTo.getParameter(Parameter.RELTYPE) as RelType?)) { RelType.CHILD -> Relation.RELTYPE_CHILD RelType.SIBLING -> Relation.RELTYPE_SIBLING - else /* RelType.PARENT, default value */ -> + else *//* RelType.PARENT, default value *//* -> Relation.RELTYPE_PARENT } val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri()) @@ -266,7 +272,7 @@ class DmfsTaskBuilder( .withValue(Relation.RELATED_TYPE, relType) logger.log(Level.FINE, "Inserting relation", builder.build()) batch += builder - } + }*/ } private fun insertUnknownProperties(batch: TasksBatchOperation, idxTask: Int?) { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index 7a775f8f..b61f95d3 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -88,7 +88,10 @@ class DmfsTaskProcessor( values.getAsInteger(Tasks.PRIORITY)?.let { to.priority = it } - to.classification = when (values.getAsInteger(Tasks.CLASSIFICATION)) { + TODO("ical4j 4.x") + // Note: big method – maybe split? Depends on how we want to proceed with refactoring. + + /*to.classification = when (values.getAsInteger(Tasks.CLASSIFICATION)) { Tasks.CLASSIFICATION_PUBLIC -> Clazz.PUBLIC Tasks.CLASSIFICATION_PRIVATE -> Clazz.PRIVATE Tasks.CLASSIFICATION_CONFIDENTIAL -> Clazz.CONFIDENTIAL @@ -160,7 +163,7 @@ class DmfsTaskProcessor( to.exDates += AndroidTimeUtils.androidStringToRecurrenceSet(it, tzRegistry, allDay) { dates -> ExDate(dates) } } - values.getAsString(Tasks.RRULE)?.let { to.rRule = RRule(it) } + values.getAsString(Tasks.RRULE)?.let { to.rRule = RRule(it) }*/ } fun populateProperty(row: ContentValues, to: Task) { @@ -183,10 +186,11 @@ class DmfsTaskProcessor( } private fun populateAlarm(row: ContentValues, to: Task) { - val props = PropertyList() + val props = PropertyList() val trigger = Trigger(java.time.Duration.ofMinutes(-row.getAsLong(Alarm.MINUTES_BEFORE))) - when (row.getAsInteger(Alarm.REFERENCE)) { + TODO("ical4j 4.x") + /*when (row.getAsInteger(Alarm.REFERENCE)) { Alarm.ALARM_REFERENCE_START_DATE -> trigger.parameters.add(Related.START) Alarm.ALARM_REFERENCE_DUE_DATE -> @@ -204,7 +208,7 @@ class DmfsTaskProcessor( Action.DISPLAY } - props += Description(row.getAsString(Alarm.MESSAGE) ?: to.summary) + props += Description(row.getAsString(Alarm.MESSAGE) ?: to.summary)*/ to.alarms += VAlarm(props) } @@ -219,14 +223,15 @@ class DmfsTaskProcessor( val relatedTo = RelatedTo(uid) // add relation type as reltypeparam - relatedTo.parameters.add(when (row.getAsInteger(Relation.RELATED_TYPE)) { + TODO("ical4j 4.x") + /*relatedTo.parameters.add(when (row.getAsInteger(Relation.RELATED_TYPE)) { Relation.RELTYPE_CHILD -> RelType.CHILD Relation.RELTYPE_SIBLING -> RelType.SIBLING - else /* Relation.RELTYPE_PARENT, default value */ -> + else *//* Relation.RELTYPE_PARENT, default value *//* -> RelType.PARENT - }) + })*/ to.relatedTo.add(relatedTo) } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt index 3159c2f7..49818680 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt @@ -66,11 +66,12 @@ object AndroidTimeUtils { * @param date [net.fortuna.ical4j.model.property.DateProperty] to validate. Values which are not DATE-TIME will be ignored. * @param tzRegistry time zone registry to get time zones from */ - fun androidifyTimeZone(date: DateProperty?, tzRegistry: TimeZoneRegistry) { - if (DateUtils.isDateTime(date) && date?.isUtc == false) { + fun androidifyTimeZone(date: DateProperty<*>?, tzRegistry: TimeZoneRegistry) { + TODO("ical4j 4.x") + /*if (DateUtils.isDateTime(date) && date?.isUtc == false) { val tzID = DateUtils.findAndroidTimezoneID(date.timeZone?.id) date.timeZone = tzRegistry.getTimeZone(tzID) - } + }*/ } /** @@ -81,11 +82,12 @@ object AndroidTimeUtils { * * * @param dateList [net.fortuna.ical4j.model.property.DateListProperty] to validate. Values which are not DATE-TIME will be ignored. */ - fun androidifyTimeZone(dateList: DateListProperty) { + fun androidifyTimeZone(dateList: DateListProperty<*>) { val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() } // periods (RDate only) - val periods = (dateList as? RDate)?.periods + TODO("ical4j 4.x") + /*val periods = (dateList as? RDate)?.periods if (periods != null && periods.isNotEmpty() && !periods.isUtc) { val tzID = DateUtils.findAndroidTimezoneID(periods.timeZone?.id) @@ -103,7 +105,7 @@ object AndroidTimeUtils { val tzID = DateUtils.findAndroidTimezoneID(dates.timeZone?.id) dateList.timeZone = tzRegistry.getTimeZone(tzID) } - } + }*/ } /** @@ -118,23 +120,24 @@ object AndroidTimeUtils { * - the specified time zone ID for date-times with given time zone * - the currently set default time zone ID for floating date-times */ - fun storageTzId(date: DateProperty): String = - if (DateUtils.isDateTime(date)) { - // DATE-TIME - when { - date.isUtc -> - // DATE-TIME in UTC format - TimeZones.UTC_ID - date.timeZone != null -> - // DATE-TIME with given time-zone - date.timeZone.id - else -> - // DATE-TIME in local format (floating) - TimeZone.getDefault().id - } - } else - // DATE - TZID_UTC + fun storageTzId(date: DateProperty<*>): String = + TODO("ical4j 4.x") + /*if (DateUtils.isDateTime(date)) { + // DATE-TIME + when { + date.isUtc -> + // DATE-TIME in UTC format + TimeZones.UTC_ID + date.timeZone != null -> + // DATE-TIME with given time-zone + date.timeZone.id + else -> + // DATE-TIME in local format (floating) + TimeZone.getDefault().id + } + } else + // DATE + TZID_UTC*/ // recurrence sets @@ -159,7 +162,7 @@ object AndroidTimeUtils { * * @return formatted string for Android calendar provider */ - fun recurrenceSetsToAndroidString(dates: List, dtStart: Date): String { + fun recurrenceSetsToAndroidString(dates: List>, dtStart: Date): String { /* rdate/exdate: DATE DATE_TIME all-day store as ...T000000Z cut off time and store as ...T000000Z event with time (undefined) store as ...ThhmmssZ @@ -169,7 +172,8 @@ object AndroidTimeUtils { val allDay = dtStart !is DateTime // use time zone of first entry for the whole set; null for UTC - val tz = + TODO("ical4j 4.x") + /*val tz = (dates.firstOrNull() as? RDate)?.periods?.timeZone ?: // VALUE=PERIOD (only RDate) dates.firstOrNull()?.dates?.timeZone // VALUE=DATE/DATE-TIME @@ -223,7 +227,7 @@ object AndroidTimeUtils { if (tz != null) result.append(tz.id).append(RECURRENCE_LIST_TZID_SEPARATOR) result.append(strDates.joinToString(RECURRENCE_LIST_VALUE_SEPARATOR)) - return result.toString() + return result.toString()*/ } /** @@ -241,16 +245,18 @@ object AndroidTimeUtils { * * @throws java.text.ParseException when the string cannot be parsed */ - fun androidStringToRecurrenceSet( + fun> androidStringToRecurrenceSet( dbStr: String, tzRegistry: TimeZoneRegistry, allDay: Boolean, exclude: Long? = null, - generator: (DateList) -> T + generator: (DateList<*>) -> T ): T { + TODO("ical4j 4.x") + // 1. split string into time zone and actual dates - var timeZone: net.fortuna.ical4j.model.TimeZone? + /*var timeZone: net.fortuna.ical4j.model.TimeZone? val datesStr: String val limiter = dbStr.indexOf(RECURRENCE_LIST_TZID_SEPARATOR) @@ -289,7 +295,7 @@ object AndroidTimeUtils { property.setUtc(true) } - return property + return property*/ } /** @@ -303,8 +309,10 @@ object AndroidTimeUtils { * * @return formatted string for Android calendar provider */ - fun recurrenceSetsToOpenTasksString(dates: List, tz: net.fortuna.ical4j.model.TimeZone?): String { - val allDay = tz == null + fun recurrenceSetsToOpenTasksString(dates: List>, tz: net.fortuna.ical4j.model.TimeZone?): String { + TODO("ical4j 4.x") + + /*val allDay = tz == null val strDates = LinkedList() for (dateListProp in dates) { if (dateListProp is RDate && dateListProp.periods.isNotEmpty()) @@ -326,7 +334,7 @@ object AndroidTimeUtils { strDates += dateToUse.toString() } } - return strDates.joinToString(RECURRENCE_LIST_VALUE_SEPARATOR) + return strDates.joinToString(RECURRENCE_LIST_VALUE_SEPARATOR)*/ } diff --git a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt index c5da00dc..2059e5cd 100644 --- a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt +++ b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt @@ -1,3 +1,9 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + /* * Copyright (c) Techbee e.U. * All rights reserved. This program and the accompanying materials @@ -27,7 +33,6 @@ import at.techbee.jtx.JtxContract.JtxICalObject.GEO_LAT import at.techbee.jtx.JtxContract.JtxICalObject.GEO_LONG import at.techbee.jtx.JtxContract.JtxICalObject.TZ_ALLDAY import net.fortuna.ical4j.model.ParameterList -import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.XProperty @@ -101,8 +106,8 @@ object JtxContract { * @param [string] that should be parsed * @return The list of XProperty parsed from the string */ - fun getXPropertyListFromJson(string: String): PropertyList { - val propertyList = PropertyList() + fun getXPropertyListFromJson(string: String): PropertyList { + val propertyList = PropertyList() if (string.isBlank()) return propertyList @@ -136,9 +141,12 @@ object JtxContract { return null val jsonObject = JSONObject() - parameters.forEach { parameter -> + TODO("ical4j 4.x") + // Note: probably the contract should be separated from methods that do things, especially if they depend on ical4j + + /*parameters.forEach { parameter -> jsonObject.put(parameter.name, parameter.value) - } + }*/ return if (jsonObject.length() == 0) null else @@ -151,18 +159,21 @@ object JtxContract { * @param [propertyList] The PropertyList that should be transformed into a Json String * @return The generated Json object as a [String] */ - fun getJsonStringFromXProperties(propertyList: PropertyList<*>?): String? { + fun getJsonStringFromXProperties(propertyList: PropertyList?): String? { if (propertyList == null) return null - val jsonObject = JSONObject() + TODO("ical4j 4.x") + // Note: probably the contract should be separated from methods that do things, especially if they depend on ical4j + + /*val jsonObject = JSONObject() propertyList.forEach { property -> jsonObject.put(property.name, property.value) } return if (jsonObject.length() == 0) null else - jsonObject.toString() + jsonObject.toString()*/ } diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt index 6b818bde..aae7649a 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -50,11 +50,12 @@ class ICalendarTest { private fun readTimeZone(fileName: String): VTimeZone { - javaClass.classLoader!!.getResourceAsStream("tz/$fileName").use { tzStream -> + TODO("ical4j 4.x") + /*javaClass.classLoader!!.getResourceAsStream("tz/$fileName").use { tzStream -> val cal = CalendarBuilder().build(tzStream) val vTimeZone = cal.getComponent(Component.VTIMEZONE) as VTimeZone return vTimeZone - } + }*/ } @Test @@ -71,9 +72,10 @@ class ICalendarTest { "END:VCALENDAR" ) ) - assertEquals("Some Calendar", calendar.getProperty(ICalendar.CALENDAR_NAME).value) + TODO("ical4j 4.x") + /*assertEquals("Some Calendar", calendar.getProperty(ICalendar.CALENDAR_NAME).value) assertEquals("darkred", calendar.getProperty(Color.PROPERTY_NAME).value) - assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).value) + assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).value)*/ } @Test @@ -217,7 +219,11 @@ class ICalendarTest { } - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun testVAlarmToMin_TriggerDuration_Negative() { // TRIGGER;REL=START:-P1DT1H1M29S val (ref, min) = ICalendar.vAlarmToMin( @@ -306,7 +312,7 @@ class ICalendarTest { false )!! assertEquals(Related.START, ref) - assertEquals(-(60 * 24 + 60 + 1 + 1) /* duration of event: */ - 1, min) + assertEquals(-(60 * 24 + 60 + 1 + 1) *//* duration of event: *//* - 1, min) } @Test @@ -328,7 +334,10 @@ class ICalendarTest { val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(DateTime(currentTime)), null, null, false)!! assertEquals(Related.START, ref) assertEquals(1, min) - } + }*/ + + + // TODO Note: can we use the following now when we have ical4j 4.x? /* DOES NOT WORK YET! Will work as soon as Java 8 API is consequently used in ical4j and ical4android. diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt index 3d8397ff..3af07a22 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt @@ -31,7 +31,8 @@ class Ical4jServiceLoaderTest { "END:VCALENDAR\n" val result = CalendarBuilder().build(StringReader(iCal)) val vEvent = result.getComponent(Component.VEVENT) - assertEquals("Networld+Interop Conference", vEvent.summary.value) + TODO("ical4j 4.x") + //assertEquals("Networld+Interop Conference", vEvent.summary.value) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt index 8360c02a..a1d81b8d 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt @@ -62,8 +62,9 @@ class TaskReaderTest { "END:VCALENDAR\r\n") assertEquals("DTSTART is DATE, but DUE is DATE-TIME", t.summary) // rewrite DTSTART to DATE-TIME, too - assertEquals(DtStart(DateTime("20200731T000000", tzVienna)), t.dtStart) - assertEquals(Due(DateTime("20200731T234600", tzVienna)), t.due) + TODO("ical4j 4.x") + /*assertEquals(DtStart(DateTime("20200731T000000", tzVienna)), t.dtStart) + assertEquals(Due(DateTime("20200731T234600", tzVienna)), t.due)*/ } @Test @@ -78,8 +79,9 @@ class TaskReaderTest { "END:VCALENDAR\r\n") assertEquals("DTSTART is DATE-TIME, but DUE is DATE", t.summary) // rewrite DTSTART to DATE-TIME, too - assertEquals(DtStart(DateTime("20200731T235510", tzVienna)), t.dtStart) - assertEquals(Due(DateTime("20200801T000000", tzVienna)), t.due) + TODO("ical4j 4.x") + /*assertEquals(DtStart(DateTime("20200731T235510", tzVienna)), t.dtStart) + assertEquals(Due(DateTime("20200801T000000", tzVienna)), t.due)*/ } @Test @@ -95,7 +97,8 @@ class TaskReaderTest { assertEquals("DUE before DTSTART", t.summary) // invalid tasks with DUE before DTSTART: DTSTART should be set to null assertNull(t.dtStart) - assertEquals(Due(DateTime("20200731T123000", tzVienna)), t.due) + TODO("ical4j 4.x") + //assertEquals(Due(DateTime("20200731T123000", tzVienna)), t.due) } @Test @@ -126,7 +129,11 @@ class TaskReaderTest { } - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun testSamples() { val t = regenerate(parseCalendarFile("rfc5545-sample1.ics")) assertEquals(2, t.sequence) @@ -190,11 +197,11 @@ class TaskReaderTest { assertEquals("most-fields2@example.com", t.uid) assertEquals(DtStart(DateTime("20100101T101010Z")), t.dtStart) assertEquals( - net.fortuna.ical4j.model.property.Duration(Duration.ofSeconds(4 * 86400 + 3 * 3600 + 2 * 60 + 1) /*Dur(4, 3, 2, 1)*/), + net.fortuna.ical4j.model.property.Duration(Duration.ofSeconds(4 * 86400 + 3 * 3600 + 2 * 60 + 1) *//*Dur(4, 3, 2, 1)*//*), t.duration ) assertTrue(t.unknownProperties.isEmpty()) - } + }*/ /* helpers */ diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt index 977ed15e..7837d795 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt @@ -21,7 +21,8 @@ class TaskTest { assertTrue(Task().isAllDay()) // DTSTART has priority - assertFalse(Task().apply { + TODO("ical4j 4.x") + /*assertFalse(Task().apply { dtStart = DtStart(DateTime()) }.isAllDay()) assertFalse(Task().apply { @@ -42,7 +43,7 @@ class TaskTest { }.isAllDay()) assertTrue(Task().apply { due = Due(Date()) - }.isAllDay()) + }.isAllDay())*/ } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt index b9095e2c..8854c74e 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt @@ -29,10 +29,11 @@ class TaskWriterTest { fun testWrite() { val t = Task() t.uid = "SAMPLEUID" - t.dtStart = DtStart("20190101T100000", tzBerlin) + TODO("ical4j 4.x") + //t.dtStart = DtStart("20190101T100000", tzBerlin) val alarm = VAlarm(Duration.ofHours(-1) /*Dur(0, -1, 0, 0)*/) - alarm.properties += Action.AUDIO + //alarm.properties += Action.AUDIO t.alarms += alarm val icalWriter = StringWriter() diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt index 11c831ca..1ca07c43 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt @@ -38,9 +38,10 @@ class UnknownPropertyTest { assertTrue(prop is Attendee) assertEquals("ATTENDEE", prop.name) assertEquals("PropValue", prop.value) - assertEquals(2, prop.parameters.size()) + TODO("ical4j 4.x") + /*assertEquals(2, prop.parameters.size()) assertEquals("value1", prop.parameters.getParameter("x-param1").value) - assertEquals("value2", prop.parameters.getParameter("x-param2").value) + assertEquals("value2", prop.parameters.getParameter("x-param2").value)*/ } @Test(expected = JSONException::class) @@ -59,8 +60,9 @@ class UnknownPropertyTest { attendee.toString().trim() ) - attendee.parameters.add(Rsvp(true)) - attendee.parameters.add(XParameter("X-My-Param", "SomeValue")) + TODO("ical4j 4.x") + /*attendee.parameters.add(Rsvp(true)) + attendee.parameters.add(XParameter("X-My-Param", "SomeValue"))*/ assertEquals( "ATTENDEE;RSVP=TRUE;X-My-Param=SomeValue:mailto:test@test.at", attendee.toString().trim() diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt index 9163a750..859f02ff 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt @@ -40,15 +40,17 @@ class DateUtilsTest { @Test fun testIsDate() { - assertTrue(DateUtils.isDate(DtStart(Date("20200101")))) - assertFalse(DateUtils.isDate(DtStart(DateTime("20200101T010203Z")))) + TODO("ical4j 4.x") + /*assertTrue(DateUtils.isDate(DtStart(Date("20200101")))) + assertFalse(DateUtils.isDate(DtStart(DateTime("20200101T010203Z"))))*/ assertFalse(DateUtils.isDate(null)) } @Test fun testIsDateTime() { - assertFalse(DateUtils.isDateTime(DtEnd(Date("20200101")))) - assertTrue(DateUtils.isDateTime(DtEnd(DateTime("20200101T010203Z")))) + TODO("ical4j 4.x") + /*assertFalse(DateUtils.isDateTime(DtEnd(Date("20200101")))) + assertTrue(DateUtils.isDateTime(DtEnd(DateTime("20200101T010203Z"))))*/ assertFalse(DateUtils.isDateTime(null)) } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt index 679b6b9a..8aba346a 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt @@ -19,7 +19,11 @@ class AssociatedComponentsTest { AssociatedEvents(null, emptyList()) } - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun testOnlyExceptions_UidNull() { AssociatedEvents(null, listOf( VEvent(propertyListOf( @@ -74,6 +78,6 @@ class AssociatedComponentsTest { Uid("test1"), RecurrenceId(Date("20250629")) )), emptyList()) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt index ce06de1f..bc4d55ba 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt @@ -30,7 +30,8 @@ class CalendarUidSplitterTest { @Test fun testAssociatedVEventsByUid_ExceptionOnly_NoUid() { val exception = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z") + TODO("ical4j 4.x") + //RecurrenceId("20250629T000000Z") )) val calendar = Calendar(componentListOf(exception)) val result = CalendarUidSplitter().associateByUid(calendar, Component.VEVENT) @@ -77,7 +78,11 @@ class CalendarUidSplitterTest { assertEquals(emptyList(), result) } - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun testFilterBySequence_MainAndExceptions_MultipleSequences() { val mainEvent1a = VEvent(propertyListOf(Sequence(1))) val mainEvent1b = VEvent(propertyListOf(Sequence(2))) @@ -159,7 +164,7 @@ class CalendarUidSplitterTest { listOf(exception1a, exception1c, exception1b, exception2a, exception2b) ) assertEquals(listOf(exception1c, exception2b), result) - } + }*/ @Test fun testFilterBySequence_OnlyMain_SingleSequence() { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt index cc0561a6..bf49c630 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt @@ -36,7 +36,8 @@ class ICalendarGeneratorTest { @Test fun `Write event with exceptions and various timezones`() { val iCal = StringWriter() - writer.write(AssociatedEvents( + TODO("ical4j 4.x") + /*writer.write(AssociatedEvents( main = VEvent(propertyListOf( Uid("SAMPLEUID"), @@ -57,7 +58,7 @@ class ICalendarGeneratorTest { )) ), prodId = userAgent - ), iCal) + ), iCal)*/ assertEquals("BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt index 6dd6d04d..757b3fec 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt @@ -25,7 +25,6 @@ import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId -import net.fortuna.ical4j.transform.rfc5545.DatePropertyRule import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull @@ -53,8 +52,9 @@ class Ical4jTest { "END:VCALENDAR" ) ).getComponent(Component.VEVENT) - val attendee = event.getProperty(Property.ATTENDEE) - assertEquals("attendee1@example.virtual", attendee.getParameter(Parameter.EMAIL).value) + TODO("ical4j 4.x") + /*val attendee = event.getProperty(Property.ATTENDEE) + assertEquals("attendee1@example.virtual", attendee.getParameter(Parameter.EMAIL).value)*/ } @Test @@ -88,7 +88,8 @@ class Ical4jTest { val iCalFromKOrganizer = CalendarBuilder().build(StringReader(vtzFromKOrganizer)) ICalPreprocessor().preprocessCalendar(iCalFromKOrganizer) val vEvent = iCalFromKOrganizer.getComponent(Component.VEVENT) - val dtStart = vEvent.startDate + TODO("ical4j 4.x") + /*val dtStart = vEvent.startDate*/ // SHOULD BE UTC -3: // assertEquals(1756396800000, dtStart.date.time) // However is one hour later: 1756400400000 @@ -97,7 +98,8 @@ class Ical4jTest { @Test fun `PRODID is folded when exactly max line length`() { val calendar = Calendar().apply { - properties += ProdId("01234567890123456789012345678901234567890123456789012345678901234567") + TODO("ical4j 4.x") + //properties += ProdId("01234567890123456789012345678901234567890123456789012345678901234567") } val writer = StringWriter() CalendarOutputter().output(calendar, writer) @@ -165,9 +167,10 @@ class Ical4jTest { "END:VTIMEZONE\n" + "END:VCALENDAR" val iCalFromGoogle = CalendarBuilder().build(StringReader(vtzFromGoogle)) - val dublinFromGoogle = iCalFromGoogle.getComponent(Component.VTIMEZONE) as VTimeZone + TODO("ical4j 4.x") + /*val dublinFromGoogle = iCalFromGoogle.getComponent(Component.VTIMEZONE) as VTimeZone val dt = DateTime("20210108T151500", TimeZone(dublinFromGoogle)) - assertEquals("20210108T151500", dt.toString()) + assertEquals("20210108T151500", dt.toString())*/ } @Test @@ -218,17 +221,18 @@ class Ical4jTest { val event = cal.getComponent(Component.VEVENT) val tzGMT5 = tzRegistry.getTimeZone("(GMT -05:00)") assertNotNull(tzGMT5) - assertEquals(DtStart("20250124T190000", tzGMT5), event.startDate) + TODO("ical4j 4.x") + /*assertEquals(DtStart("20250124T190000", tzGMT5), event.startDate) assertEquals(DtEnd("20250124T203000", tzGMT5), event.endDate) // now apply DatePropertyRule DatePropertyRule().applyTo(event.startDate) DatePropertyRule().applyTo(event.endDate) - /* "(GMT -05:00)" is neither in msTimezones, nor in IANA timezones, so - DatePropertyRule completely removes it, but keeps the offset. */ + *//* "(GMT -05:00)" is neither in msTimezones, nor in IANA timezones, so + DatePropertyRule completely removes it, but keeps the offset. *//* assertEquals(DtStart(DateTime("20250125T000000Z")), event.startDate) - assertEquals(DtEnd(DateTime("20250125T013000Z")), event.endDate) + assertEquals(DtEnd(DateTime("20250125T013000Z")), event.endDate)*/ } @Test(expected = ParserException::class) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt index a8076d96..85a83af2 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt @@ -54,11 +54,12 @@ class ICalPreprocessorTest { javaClass.getResourceAsStream("/events/outlook1.ics").use { stream -> val reader = InputStreamReader(stream, Charsets.UTF_8) val calendar = CalendarBuilder().build(reader) - val vEvent = calendar.getComponent(Component.VEVENT) as VEvent + TODO("ical4j 4.x") + /*val vEvent = calendar.getComponent(Component.VEVENT) as VEvent assertEquals("W. Europe Standard Time", vEvent.startDate.timeZone.id) processor.preprocessCalendar(calendar) - assertEquals("Europe/Vienna", vEvent.startDate.timeZone.id) + assertEquals("Europe/Vienna", vEvent.startDate.timeZone.id)*/ } } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt index 18a765de..1904a93e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt @@ -42,9 +42,13 @@ class AndroidEventHandlerTest { private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna")!! + init { + TODO("ical4j 4.x") + } + // mapToVEvents → MappingResult.associatedEvents - @Test + /*@Test fun `mapToVEvents processes exceptions`() { val result = handler.mapToVEvents( eventAndExceptions = EventAndExceptions( @@ -283,6 +287,6 @@ class AndroidEventHandlerTest { assertFalse(result.generatedUid) assertEquals("sample-uid", result.uid) assertEquals("sample-uid", result.associatedEvents.main?.uid?.value) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt index 8f8ea0e5..e533c616 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt @@ -27,8 +27,11 @@ class AttendeeMappingsTest { val RoleFancy = Role("X-FANCY") } + init { + TODO("ical4j 4.x") + } - @Test + /*@Test fun testAndroidToICalendar_TypeRequired_RelationshipAttendee() { testAndroidToICalendar(ContentValues().apply { put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED) @@ -1126,6 +1129,6 @@ class AttendeeMappingsTest { val attendee = Attendee() AttendeeMappings.androidToICalendar(values, attendee) test(attendee) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt index 0451ad41..36a5bd16 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt @@ -43,11 +43,12 @@ class AccessLevelBuilderTest { @Test fun `Classification is PUBLIC`() { val result = Entity(ContentValues()) - builder.build( + TODO("ical4j 4.x") + /*builder.build( from = VEvent(propertyListOf(Clazz.PUBLIC)), main = VEvent(), to = result - ) + )*/ assertContentValuesEqual(contentValuesOf( Events.ACCESS_LEVEL to Events.ACCESS_PUBLIC ), result.entityValues) @@ -57,11 +58,12 @@ class AccessLevelBuilderTest { @Test fun `Classification is PRIVATE`() { val result = Entity(ContentValues()) - builder.build( + TODO("ical4j 4.x") + /*builder.build( from = VEvent(propertyListOf(Clazz.PRIVATE)), main = VEvent(), to = result - ) + )*/ assertContentValuesEqual(contentValuesOf( Events.ACCESS_LEVEL to Events.ACCESS_PRIVATE ), result.entityValues) @@ -71,11 +73,12 @@ class AccessLevelBuilderTest { @Test fun `Classification is CONFIDENTIAL`() { val result = Entity(ContentValues()) - builder.build( + TODO("ical4j 4.x") + /*builder.build( from = VEvent(propertyListOf(Clazz.CONFIDENTIAL)), main = VEvent(), to = result - ) + )*/ assertContentValuesEqual(contentValuesOf( Events.ACCESS_LEVEL to Events.ACCESS_CONFIDENTIAL diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt index ce2e79ff..bd9b4a54 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt @@ -41,11 +41,12 @@ class AllDayBuilderTest { @Test fun `DTSTART is DATE`() { val result = Entity(ContentValues()) - builder.build( + TODO("ical4j 4.x") + /*builder.build( from = VEvent(propertyListOf(DtStart(Date()))), main = VEvent(), to = result - ) + )*/ assertContentValuesEqual(contentValuesOf( Events.ALL_DAY to 1 ), result.entityValues) @@ -54,11 +55,12 @@ class AllDayBuilderTest { @Test fun `DTSTART is DATE-TIME`() { val result = Entity(ContentValues()) - builder.build( + TODO("ical4j 4.x") + /*builder.build( from = VEvent(propertyListOf(DtStart(DateTime()))), main = VEvent(), to = result - ) + )*/ assertContentValuesEqual(contentValuesOf( Events.ALL_DAY to 0 ), result.entityValues) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt index 0797fa34..63296feb 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt @@ -39,7 +39,11 @@ class AttendeesBuilderTest { private val builder = AttendeesBuilder(mockCalendar) - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Attendee is email address`() { val result = Entity(ContentValues()) builder.build( @@ -524,6 +528,6 @@ class AttendeesBuilderTest { result.subValues.first { it.uri == Attendees.CONTENT_URI }.values, onlyFieldsInExpected = true ) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt index 45c7e0c1..a33c63df 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt @@ -38,22 +38,24 @@ class AvailabilityBuilderTest { @Test fun `Transparency is OPAQUE`() { val result = Entity(ContentValues()) - builder.build( + TODO("ical4j 4.x") + /*builder.build( from = VEvent(propertyListOf(Transp.OPAQUE)), main = VEvent(), to = result - ) + )*/ assertEquals(Events.AVAILABILITY_BUSY, result.entityValues.get(Events.AVAILABILITY)) } @Test fun `Transparency is TRANSPARENT`() { val result = Entity(ContentValues()) - builder.build( + TODO("ical4j 4.x") + /*builder.build( from = VEvent(propertyListOf(Transp.TRANSPARENT)), main = VEvent(), to = result - ) + )*/ assertEquals(Events.AVAILABILITY_FREE, result.entityValues.get(Events.AVAILABILITY)) } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt index 8a9e8af3..1b72adf0 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt @@ -31,7 +31,8 @@ class CategoriesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Categories(TextList(arrayOf("Cat 1", "Cat\\2"))) + TODO("ical4j 4.x") + // properties += Categories(TextList(arrayOf("Cat 1", "Cat\\2"))) }, main = VEvent(), to = result diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt index 39005f7b..2403e1ae 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt @@ -34,7 +34,11 @@ class DurationBuilderTest { private val builder = DurationBuilder() - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Not a main event`() { val result = Entity(ContentValues()) builder.build(VEvent(propertyListOf( @@ -319,6 +323,6 @@ class DurationBuilderTest { java.time.Duration.ofHours(2), result ) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt index 2afb2310..ee909f07 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt @@ -36,7 +36,11 @@ class EndTimeBuilderTest { private val builder = EndTimeBuilder() - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Recurring event`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( @@ -322,6 +326,6 @@ class EndTimeBuilderTest { DtEnd(DateTime("20250102T055623", tzVienna)), result ) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt index 48da87b7..00500711 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt @@ -46,7 +46,8 @@ class OrganizerBuilderTest { builder.build( from = VEvent(propertyListOf(Organizer("mailto:organizer@example.com"))).apply { // at least one attendee to make event group-scheduled - properties += Attendee("mailto:attendee@example.com") + TODO("ical4j 4.x") + // properties += Attendee("mailto:attendee@example.com") }, main = VEvent(), to = result @@ -63,7 +64,8 @@ class OrganizerBuilderTest { builder.build( from = VEvent(propertyListOf(Organizer("local-id:user"))).apply { // at least one attendee to make event group-scheduled - properties += Attendee("mailto:attendee@example.com") + TODO("ical4j 4.x") + // properties += Attendee("mailto:attendee@example.com") }, main = VEvent(), to = result @@ -80,7 +82,8 @@ class OrganizerBuilderTest { builder.build( from = VEvent(propertyListOf( Organizer("local-id:user").apply { - parameters.add(Email("organizer@example.com")) + TODO("ical4j 4.x") + // parameters.add(Email("organizer@example.com")) }, Attendee("mailto:attendee@example.com") )), diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt index 5ac06c10..d591175f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt @@ -31,7 +31,11 @@ class OriginalInstanceTimeBuilderTest { private val builder = OriginalInstanceTimeBuilder() - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Main event`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf(DtStart())) @@ -124,6 +128,6 @@ class OriginalInstanceTimeBuilderTest { Events.ORIGINAL_ALL_DAY to 0, Events.ORIGINAL_INSTANCE_TIME to 1594143000000L ), result.entityValues) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt index abeb030d..fc2657ff 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt @@ -31,7 +31,11 @@ class RecurrenceFieldsBuilderTest { private val builder = RecurrenceFieldsBuilder() - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Exception event`() { // Exceptions (of recurring events) must never have recurrence properties themselves. val result = Entity(ContentValues()) @@ -199,6 +203,6 @@ class RecurrenceFieldsBuilderTest { Events.EXRULE to null, Events.EXDATE to "20250920T000000Z" ), result.entityValues) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt index d6d9bbb4..be87283b 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt @@ -36,7 +36,11 @@ class RemindersBuilderTest { private val builder = RemindersBuilder() - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `No trigger`() { val result = Entity(ContentValues()) builder.build( @@ -248,7 +252,7 @@ class RemindersBuilderTest { to = result ) assertReminder(result, Reminders.MINUTES to 420) - } + }*/ // helpers diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt index caf79408..0bd60e27 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt @@ -39,7 +39,11 @@ class StartTimeBuilderTest { } - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `All-day event`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( @@ -77,6 +81,6 @@ class StartTimeBuilderTest { )) builder.build(event, event, result) assertEquals(1760050923000, result.entityValues.get(Events.DTSTART)) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt index 3761bf6b..47db3c5f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt @@ -36,7 +36,11 @@ class StatusBuilderTest { assertNull(result.entityValues.get(Events.STATUS)) } - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `STATUS is CONFIRMED`() { val result = Entity(ContentValues()) builder.build( @@ -78,6 +82,6 @@ class StatusBuilderTest { to = result ) assertEquals(Events.STATUS_TENTATIVE, result.entityValues.getAsInteger(Events.STATUS)) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt index a56a7e3a..61b72ffc 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt @@ -46,8 +46,9 @@ class UnknownPropertiesBuilderTest { builder.build( from = VEvent(propertyListOf( XProperty("X-Some-Property", "Some Value").apply { - parameters.add(XParameter("Param1", "Value1")) - parameters.add(XParameter("Param2", "Value2")) + TODO("ical4j 4.x") + /*parameters.add(XParameter("Param1", "Value1")) + parameters.add(XParameter("Param2", "Value2"))*/ } )), main = VEvent(), diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt index 7eb438fd..d2a73979 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt @@ -76,7 +76,8 @@ class AccessLevelHandlerTest { Events.ACCESS_LEVEL to Events.ACCESS_PUBLIC )) handler.process(entity, entity, result) - assertEquals(Clazz.PUBLIC, result.classification) + TODO("ical4j 4.x") + //assertEquals(Clazz.PUBLIC, result.classification) } @Test @@ -86,7 +87,8 @@ class AccessLevelHandlerTest { Events.ACCESS_LEVEL to Events.ACCESS_PRIVATE )) handler.process(entity, entity, result) - assertEquals(Clazz.PRIVATE, result.classification) + TODO("ical4j 4.x") + //assertEquals(Clazz.PRIVATE, result.classification) } @Test @@ -96,7 +98,8 @@ class AccessLevelHandlerTest { Events.ACCESS_LEVEL to Events.ACCESS_CONFIDENTIAL )) handler.process(entity, entity, result) - assertEquals(Clazz.CONFIDENTIAL, result.classification) + TODO("ical4j 4.x") + //assertEquals(Clazz.CONFIDENTIAL, result.classification) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt index b8d244af..c0404a18 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt @@ -32,7 +32,11 @@ class AttendeesHandlerTest { private val handler = AttendeesHandler() - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Attendee is email address`() { val entity = Entity(ContentValues()) entity.addSubValue(Attendees.CONTENT_URI, contentValuesOf( @@ -329,6 +333,6 @@ class AttendeesHandlerTest { handler.process(entity, entity, result) val attendee = result.getProperty(Property.ATTENDEE) assertTrue(attendee.getParameter(Parameter.RSVP).rsvp) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt index db72c57d..cce14704 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt @@ -51,7 +51,8 @@ class AvailabilityHandlerTest { Events.AVAILABILITY to Events.AVAILABILITY_FREE )) handler.process(entity, entity, result) - assertEquals(Transp.TRANSPARENT, result.getProperty(Property.TRANSP)) + TODO("ical4j 4.x") + //assertEquals(Transp.TRANSPARENT, result.getProperty(Property.TRANSP)) } @Test diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt index ba640d72..75b45972 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt @@ -42,7 +42,8 @@ class CategoriesHandlerTest { ExtendedProperties.VALUE to "Cat 1\\Cat 2" )) handler.process(entity, entity, result) - assertEquals(listOf("Cat 1", "Cat 2"), result.getProperty(Property.CATEGORIES).categories.toList()) + TODO("ical4j 4.x") + //assertEquals(listOf("Cat 1", "Cat 2"), result.getProperty(Property.CATEGORIES).categories.toList()) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt index 0f6e3586..28240bf0 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt @@ -39,7 +39,8 @@ class ColorHandlerTest { Events.EVENT_COLOR_KEY to Css3Color.silver.name )) handler.process(entity, entity, result) - assertEquals("silver", result.getProperty(Color.PROPERTY_NAME).value) + TODO("ical4j 4.x") + //assertEquals("silver", result.getProperty(Color.PROPERTY_NAME).value) } @Test @@ -49,7 +50,8 @@ class ColorHandlerTest { Events.EVENT_COLOR to Css3Color.silver.argb )) handler.process(entity, entity, result) - assertEquals("silver", result.getProperty(Color.PROPERTY_NAME).value) + TODO("ical4j 4.x") + //assertEquals("silver", result.getProperty(Color.PROPERTY_NAME).value) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt index 17e351cb..d70bb7ac 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt @@ -28,9 +28,13 @@ class DurationHandlerTest { private val handler = DurationHandler(tzRegistry) + init { + TODO("ical4j 4.x") + } + // Note: When the calendar provider sets a non-null DURATION, it implies that the event is recurring. - @Test + /*@Test fun `All-day event with all-day duration`() { val result = VEvent() val entity = Entity(contentValuesOf( @@ -177,6 +181,6 @@ class DurationHandlerTest { handler.process(entity, entity, result) assertNull(result.endDate) assertNull(result.duration) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt index e470dff0..39d25af5 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt @@ -35,7 +35,11 @@ class EndTimeHandlerTest { // Note: When the calendar provider sets a non-null DTEND, it implies that the event is not recurring. - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `All-day event`() { val result = VEvent() val entity = Entity(contentValuesOf( @@ -157,6 +161,6 @@ class EndTimeHandlerTest { val start = System.currentTimeMillis() val result = handler.calculateFromDefault(start, allDay = false) assertEquals(start, result) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt index 507c6532..98159d13 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt @@ -28,7 +28,11 @@ class OriginalInstanceTimeHandlerTest { private val handler = OriginalInstanceTimeHandler(tzRegistry) - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Original event is all-day`() { val result = VEvent() val entity = Entity(contentValuesOf( @@ -49,6 +53,6 @@ class OriginalInstanceTimeHandlerTest { )) handler.process(entity, Entity(ContentValues()), result) assertEquals(RecurrenceId(DateTime("20250922T161348", tzVienna)), result.recurrenceId) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt index 9e4f3898..cdc76fb3 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt @@ -39,7 +39,11 @@ class RecurrenceFieldHandlerTest { private val handler = RecurrenceFieldsHandler(tzRegistry) - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Recurring exception`() { val result = VEvent() val entity = Entity(contentValuesOf( @@ -236,6 +240,6 @@ class RecurrenceFieldHandlerTest { .build(), result ) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt index 8eb06b96..c3d6538e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt @@ -26,7 +26,11 @@ class RemindersHandlerTest { private val accountName = "user@example.com" private val handler = RemindersHandler(accountName) - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun `Email reminder`() { // account name looks like an email address assumeTrue(accountName.endsWith("@example.com")) @@ -103,6 +107,6 @@ class RemindersHandlerTest { handler.process(entity, entity, result) val alarm = result.alarms.first() assertEquals(Duration.ofMinutes(10), alarm.trigger.duration) - } + }*/ } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt index 3863f27c..6ccca590 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt @@ -39,7 +39,8 @@ class StartTimeHandlerTest { Events.EVENT_TIMEZONE to AndroidTimeUtils.TZID_UTC )) handler.process(entity, entity, result) - assertEquals(DtStart(Date("20200621")), result.startDate) + TODO("ical4j 4.x") + //assertEquals(DtStart(Date("20200621")), result.startDate) } @Test @@ -50,7 +51,8 @@ class StartTimeHandlerTest { Events.EVENT_TIMEZONE to "Europe/Vienna" )) handler.process(entity, entity, result) - assertEquals(DtStart(DateTime("20200621T120000", tzVienna)), result.startDate) + TODO("ical4j 4.x") + //assertEquals(DtStart(DateTime("20200621T120000", tzVienna)), result.startDate) } @Test(expected = InvalidLocalResourceException::class) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt index 8f063514..99346fc8 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt @@ -38,7 +38,8 @@ class StatusHandlerTest { Events.STATUS to Events.STATUS_CONFIRMED )) handler.process(entity, entity, result) - assertEquals(Status.VEVENT_CONFIRMED, result.status) + TODO("ical4j 4.x") + //assertEquals(Status.VEVENT_CONFIRMED, result.status) } @Test @@ -48,7 +49,8 @@ class StatusHandlerTest { Events.STATUS to Events.STATUS_TENTATIVE )) handler.process(entity, entity, result) - assertEquals(Status.VEVENT_TENTATIVE, result.status) + TODO("ical4j 4.x") + //assertEquals(Status.VEVENT_TENTATIVE, result.status) } @Test @@ -58,7 +60,8 @@ class StatusHandlerTest { Events.STATUS to Events.STATUS_CANCELED )) handler.process(entity, entity, result) - assertEquals(Status.VEVENT_CANCELLED, result.status) + TODO("ical4j 4.x") + //assertEquals(Status.VEVENT_CANCELLED, result.status) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt index a79e4342..34514672 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt @@ -37,7 +37,8 @@ class UidHandlerTest { )) val result = VEvent() handler.process(entity, entity, result) - assertEquals("from-event", result.uid.value) + TODO("ical4j 4.x") + //assertEquals("from-event", result.uid.value) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt index ebb6d574..b37f67f8 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt @@ -30,7 +30,8 @@ class UnknownPropertiesHandlerTest { val result = VEvent(/* initialise = */ false) val entity = Entity(ContentValues()) handler.process(entity, entity, result) - assertTrue(result.properties.isEmpty()) + TODO("ical4j 4.x") + //assertTrue(result.properties.isEmpty()) } @Test @@ -50,12 +51,13 @@ class UnknownPropertiesHandlerTest { ExtendedProperties.VALUE to "[\"X-PROP2\", \"value 2\", {\"arg1\": \"arg-value\"}]" )) handler.process(entity, entity, result) - assertEquals(listOf( + TODO("ical4j 4.x") + /*assertEquals(listOf( XProperty("X-PROP1", "value 1"), XProperty("X-PROP2", "value 2").apply { parameters.add(XParameter("ARG1", "arg-value")) }, - ), result.properties) + ), result.properties)*/ } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt index c71dc1ee..c79b9aa4 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt @@ -38,7 +38,7 @@ class AndroidTimeUtilsTest { val tzBerlin: TimeZone = tzRegistry.getTimeZone("Europe/Berlin")!! val tzToronto: TimeZone = tzRegistry.getTimeZone("America/Toronto")!! - val tzCustom by lazy { + val tzCustom: TimeZone by lazy { val builder = CalendarBuilder(tzRegistry) val cal = builder.build( StringReader( @@ -54,7 +54,8 @@ class AndroidTimeUtilsTest { "END:VCALENDAR" ) ) - TimeZone(cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone) + TODO("ical4j 4.x") + //TimeZone(cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone) } val tzIdDefault = java.util.TimeZone.getDefault().id!! @@ -71,7 +72,11 @@ class AndroidTimeUtilsTest { // androidifyTimeZone // DateProperty - @Test + init { + TODO("ical4j 4.x") + } + + /*@Test fun testAndroidifyTimeZone_DateProperty_Date() { // dates (without time) should be ignored val dtStart = DtStart(Date("20150101")) @@ -493,7 +498,7 @@ class AndroidTimeUtilsTest { val list = ArrayList(1) list.add(RDate(DateList("20150101,20150702", Value.DATE))) assertEquals("20150101T000000,20150702T000000", AndroidTimeUtils.recurrenceSetsToOpenTasksString(list, tzBerlin)) - } + }*/ @Test From b15da8d7ebc4c194b1a0cf621ad4ea538822c51e Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 5 Mar 2026 10:51:20 +0100 Subject: [PATCH 02/24] Ignore tests failing due to incomplete ical4j 4.x migration (#206) --- .../bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt | 2 ++ .../kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt | 2 ++ .../at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt | 2 ++ lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt | 2 ++ .../kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt | 2 ++ lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt | 2 ++ lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt | 2 ++ lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt | 2 ++ .../test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt | 2 ++ .../test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt | 2 ++ .../at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt | 2 ++ .../at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt | 2 ++ .../at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt | 2 ++ .../at/bitfire/synctools/icalendar/ICalendarParserTest.kt | 2 ++ .../test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt | 2 ++ .../synctools/icalendar/validation/ICalPreprocessorTest.kt | 2 ++ .../synctools/mapping/calendar/AndroidEventHandlerTest.kt | 2 ++ .../bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt | 2 ++ .../mapping/calendar/builder/AccessLevelBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/AllDayBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/AttendeesBuilderTest.kt | 2 ++ .../mapping/calendar/builder/AvailabilityBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/CategoriesBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/ColorBuilderTest.kt | 2 ++ .../mapping/calendar/builder/DescriptionBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/DurationBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/EndTimeBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/LocationBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/OrganizerBuilderTest.kt | 2 ++ .../mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt | 2 ++ .../mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/RemindersBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/SequenceBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/StartTimeBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/StatusBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/TitleBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/UidBuilderTest.kt | 2 ++ .../mapping/calendar/builder/UnknownPropertiesBuilderTest.kt | 2 ++ .../synctools/mapping/calendar/builder/UrlBuilderTest.kt | 2 ++ .../mapping/calendar/handler/AccessLevelHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/AndroidTimeFieldTest.kt | 2 ++ .../synctools/mapping/calendar/handler/AttendeesHandlerTest.kt | 2 ++ .../mapping/calendar/handler/AvailabilityHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/CategoriesHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/ColorHandlerTest.kt | 2 ++ .../mapping/calendar/handler/DescriptionHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/DurationHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/EndTimeHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/LocationHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/OrganizerHandlerTest.kt | 2 ++ .../mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt | 2 ++ .../mapping/calendar/handler/RecurrenceFieldHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/RemindersHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/SequenceHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/StartTimeHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/StatusHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/TitleHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/UidHandlerTest.kt | 2 ++ .../mapping/calendar/handler/UnknownPropertiesHandlerTest.kt | 2 ++ .../synctools/mapping/calendar/handler/UrlHandlerTest.kt | 2 ++ .../kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt | 2 ++ 61 files changed, 122 insertions(+) diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt index 5663ea5e..646facc3 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt @@ -14,10 +14,12 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Test import java.time.ZoneId import java.time.zone.ZoneRulesException +@Ignore("ical4j 4.x") class AndroidCompatTimeZoneRegistryTest { lateinit var ical4jRegistry: TimeZoneRegistry diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt index 8c678a94..6bcf74d1 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt @@ -9,11 +9,13 @@ package at.bitfire.ical4android import net.fortuna.ical4j.model.TimeZoneRegistryFactory import org.junit.Assert import org.junit.Assert.assertNotNull +import org.junit.Ignore import org.junit.Test import java.time.ZoneId import java.time.format.TextStyle import java.util.Locale +@Ignore("ical4j 4.x") class AndroidTimeZonesTest { @Test diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt index e8e884d1..497a3d42 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt @@ -46,9 +46,11 @@ import org.junit.After import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before +import org.junit.Ignore import org.junit.Test import java.time.ZoneId +@Ignore("ical4j 4.x") class DmfsTaskBuilderTest ( providerName: TaskProvider.ProviderName ): DmfsStyleProvidersTaskTest(providerName) { diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt index aae7649a..0399f7ec 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -23,12 +23,14 @@ import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import java.io.StringReader import java.time.Duration import java.time.Period import java.util.Date +@Ignore("ical4j 4.x") class ICalendarTest { // UTC timezone diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt index 3af07a22..c92ca75f 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt @@ -10,9 +10,11 @@ import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Test import java.io.StringReader +@Ignore("ical4j 4.x") class Ical4jServiceLoaderTest { @Test diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt index a1d81b8d..07e3a8b9 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/TaskReaderTest.kt @@ -27,6 +27,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import java.io.InputStreamReader import java.io.StringReader @@ -34,6 +35,7 @@ import java.io.StringWriter import java.nio.charset.Charset import java.time.Duration +@Ignore("ical4j 4.x") class TaskReaderTest { val testProdId = ProdId(javaClass.name) diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt index 7837d795..0ac87b73 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/TaskTest.kt @@ -12,8 +12,10 @@ import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test +@Ignore("ical4j 4.x") class TaskTest { @Test diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt index 8854c74e..50435080 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/TaskWriterTest.kt @@ -13,10 +13,12 @@ import net.fortuna.ical4j.model.property.Action import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import java.io.StringWriter import java.time.Duration +@Ignore("ical4j 4.x") class TaskWriterTest { val testProdId = ProdId(javaClass.name) diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt index 1ca07c43..f04ca9d3 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt @@ -15,10 +15,12 @@ import net.fortuna.ical4j.model.property.Uid import org.json.JSONException import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UnknownPropertyTest { diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt index 859f02ff..828b1614 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt @@ -14,10 +14,12 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import java.time.ZoneId import java.util.TimeZone +@Ignore("ical4j 4.x") class DateUtilsTest { @Test diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt index 8aba346a..80b53cec 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/AssociatedComponentsTest.kt @@ -10,8 +10,10 @@ import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.model.property.Uid +import org.junit.Ignore import org.junit.Test +@Ignore("ical4j 4.x") class AssociatedComponentsTest { @Test(expected = IllegalArgumentException::class) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt index bc4d55ba..e0745017 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt @@ -16,8 +16,10 @@ import net.fortuna.ical4j.model.property.Sequence import net.fortuna.ical4j.model.property.Uid import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test +@Ignore("ical4j 4.x") class CalendarUidSplitterTest { @Test diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt index bf49c630..4c7307cc 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt @@ -19,10 +19,12 @@ import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.model.property.Uid import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Test import java.io.StringWriter import java.time.Duration +@Ignore("ical4j 4.x") class ICalendarGeneratorTest { private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt index 6dedfd13..c007677a 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt @@ -15,11 +15,13 @@ import io.mockk.junit4.MockKRule import io.mockk.slot import io.mockk.verify import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.io.Reader import java.io.StringReader +@Ignore("ical4j 4.x") class ICalendarParserTest { @get:Rule diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt index 757b3fec..7164522f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/Ical4jTest.kt @@ -28,11 +28,13 @@ import net.fortuna.ical4j.model.property.ProdId import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull +import org.junit.Ignore import org.junit.Test import java.io.StringReader import java.io.StringWriter import java.time.Period +@Ignore("ical4j 4.x") class Ical4jTest { private val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt index 85a83af2..4500e004 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt @@ -16,6 +16,7 @@ import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.io.InputStreamReader @@ -24,6 +25,7 @@ import java.io.StringReader import java.io.Writer import java.util.UUID +@Ignore("ical4j 4.x") class ICalPreprocessorTest { @get:Rule diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt index 1904a93e..61a15977 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandlerTest.kt @@ -25,10 +25,12 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AndroidEventHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt index e533c616..99c15246 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt @@ -13,10 +13,12 @@ import net.fortuna.ical4j.model.parameter.Role import net.fortuna.ical4j.model.property.Attendee import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AttendeeMappingsTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt index 36a5bd16..dc0fc6e7 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt @@ -17,10 +17,12 @@ import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Clazz import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AccessLevelBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt index bd9b4a54..e0c80d54 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt @@ -16,10 +16,12 @@ import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AllDayBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt index 63296feb..5a0dd554 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt @@ -24,11 +24,13 @@ import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.Organizer import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.net.URI +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AttendeesBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt index a33c63df..87393604 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt @@ -14,10 +14,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Transp import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AvailabilityBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt index 1b72adf0..06ca80be 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt @@ -17,10 +17,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Categories import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class CategoriesBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt index 1470f2f2..9cbe72ea 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt @@ -19,11 +19,13 @@ import io.mockk.junit4.MockKRule import io.mockk.mockk import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Color +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class ColorBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt index dc908764..8935b9ec 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt @@ -15,10 +15,12 @@ import net.fortuna.ical4j.model.property.Description import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class DescriptionBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt index 2403e1ae..6e65590c 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DurationBuilderTest.kt @@ -21,11 +21,13 @@ import net.fortuna.ical4j.model.property.RRule import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.Period +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class DurationBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt index ee909f07..5b6c1960 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt @@ -21,12 +21,14 @@ import net.fortuna.ical4j.model.property.RRule import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.Period import java.time.ZoneId +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class EndTimeBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt index e6149e95..a2fde917 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt @@ -15,10 +15,12 @@ import net.fortuna.ical4j.model.property.Location import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class LocationBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt index 00500711..94ce3a12 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt @@ -16,10 +16,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.Organizer +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class OrganizerBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt index d591175f..7f4ab9e4 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OriginalInstanceTimeBuilderTest.kt @@ -18,10 +18,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RecurrenceId +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class OriginalInstanceTimeBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt index fc2657ff..471bafe6 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RecurrenceFieldsBuilderTest.kt @@ -22,10 +22,12 @@ import net.fortuna.ical4j.model.property.ExDate import net.fortuna.ical4j.model.property.ExRule import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class RecurrenceFieldsBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt index be87283b..22b03f02 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt @@ -22,11 +22,13 @@ import net.fortuna.ical4j.model.property.Action import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.Period +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class RemindersBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt index b6313ce1..aa09cb10 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt @@ -14,10 +14,12 @@ import at.bitfire.synctools.storage.calendar.EventsContract import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Sequence +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class SequenceBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt index 0bd60e27..defcc647 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt @@ -17,11 +17,13 @@ import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.ZoneId +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class StartTimeBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt index 47db3c5f..afa484a6 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt @@ -15,10 +15,12 @@ import net.fortuna.ical4j.model.property.Status import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class StatusBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt index e4af9ca3..de976ea9 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt @@ -15,10 +15,12 @@ import net.fortuna.ical4j.model.property.Summary import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class TitleBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt index ac4bd73e..9acca81f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt @@ -14,10 +14,12 @@ import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Uid +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UidBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt index 61b72ffc..7b4570f7 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt @@ -20,10 +20,12 @@ import net.fortuna.ical4j.model.property.Uid import net.fortuna.ical4j.model.property.XProperty import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UnknownPropertiesBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt index 849f7e4f..3ce6a560 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt @@ -17,11 +17,13 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Url import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.net.URI +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UrlBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt index d2a73979..77ae8b3f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt @@ -16,10 +16,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Clazz import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AccessLevelHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeFieldTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeFieldTest.kt index da01b908..486e6ee8 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeFieldTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeFieldTest.kt @@ -16,9 +16,11 @@ import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assume +import org.junit.Ignore import org.junit.Test import java.time.ZoneId +@Ignore("ical4j 4.x") class AndroidTimeFieldTest { private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt index c0404a18..cf343ee5 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AttendeesHandlerTest.kt @@ -22,11 +22,13 @@ import net.fortuna.ical4j.model.property.Attendee import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.net.URI +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AttendeesHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt index cce14704..62f4f328 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AvailabilityHandlerTest.kt @@ -15,10 +15,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Transp import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AvailabilityHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt index 75b45972..982c72fc 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/CategoriesHandlerTest.kt @@ -16,10 +16,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Categories import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class CategoriesHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt index 28240bf0..049c0cf8 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt @@ -15,10 +15,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Color import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class ColorHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt index 03f91b84..c9e476c2 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt @@ -13,10 +13,12 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class DescriptionHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt index d70bb7ac..0ca6c1db 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt @@ -16,10 +16,12 @@ import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtEnd import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class DurationHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt index 39d25af5..89ac6a8f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt @@ -18,6 +18,7 @@ import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertNull import org.junit.Assume +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -25,6 +26,7 @@ import java.time.OffsetDateTime import java.time.ZoneId import java.time.ZoneOffset +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class EndTimeHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt index 547233d6..a7dd435d 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt @@ -13,10 +13,12 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class LocationHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandlerTest.kt index 54b71de2..734ef752 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OrganizerHandlerTest.kt @@ -14,10 +14,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Organizer import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class OrganizerHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt index 98159d13..7c14b022 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt @@ -16,10 +16,12 @@ import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.RecurrenceId import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class OriginalInstanceTimeHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt index cdc76fb3..1f5ba84a 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RecurrenceFieldHandlerTest.kt @@ -27,10 +27,12 @@ import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule import org.junit.Assert.assertNull import org.junit.Assert.assertSame +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class RecurrenceFieldHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt index c3d6538e..c5f20e9e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/RemindersHandlerTest.kt @@ -15,11 +15,13 @@ import net.fortuna.ical4j.model.property.Action import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assume.assumeTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.Duration +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class RemindersHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandlerTest.kt index 086b619d..757f77c7 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/SequenceHandlerTest.kt @@ -13,10 +13,12 @@ import at.bitfire.synctools.storage.calendar.EventsContract import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class SequenceHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt index 6ccca590..90d50878 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt @@ -18,10 +18,12 @@ import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class StartTimeHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt index 99346fc8..d2da6d72 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StatusHandlerTest.kt @@ -14,10 +14,12 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Status import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class StatusHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt index 8477cc76..fcc5654e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt @@ -13,10 +13,12 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class TitleHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt index 34514672..f3ba6816 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt @@ -13,10 +13,12 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UidHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt index b37f67f8..6039930c 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UnknownPropertiesHandlerTest.kt @@ -16,10 +16,12 @@ import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.XProperty import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UnknownPropertiesHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandlerTest.kt index 027cf165..b507d525 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UrlHandlerTest.kt @@ -14,11 +14,13 @@ import at.bitfire.synctools.storage.calendar.EventsContract import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.net.URI +@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UrlHandlerTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt index c79b9aa4..e8bb8604 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt @@ -28,10 +28,12 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Test import java.io.StringReader import java.time.Duration +@Ignore("ical4j 4.x") class AndroidTimeUtilsTest { val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()!! From 85f4e3cd736ef1bcd4744cbd4910c18064f62983 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Thu, 5 Mar 2026 12:26:49 +0100 Subject: [PATCH 03/24] Update CalendarUidSplitter to support ical4j 4.x (#208) * Update CalendarUidSplitter to support ical4j 4.x * Remove ignore annotation * Improve kdoc * Change Instant to LocalDate --- .../icalendar/CalendarUidSplitter.kt | 31 +++++++------- .../synctools/icalendar/Ical4jHelpers.kt | 21 +++------- .../icalendar/CalendarUidSplitterTest.kt | 42 ++++++++----------- 3 files changed, 41 insertions(+), 53 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt index 4a9e10a6..464e5e91 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitter.kt @@ -9,26 +9,31 @@ package at.bitfire.synctools.icalendar import androidx.annotation.VisibleForTesting import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.component.CalendarComponent +import kotlin.jvm.optionals.getOrNull class CalendarUidSplitter { /** - * Splits iCalendar components by UID and classifies them as main events (without RECURRENCE-ID) - * or exceptions (with RECURRENCE-ID). + * Splits iCalendar components by UID and classifies them as + * - main events (which do not have a RECURRENCE-ID) or + * - exceptions (which do have a RECURRENCE-ID). * * When there are multiple components with the same UID and RECURRENCE-ID, but different SEQUENCE, * this method keeps only the ones with the highest SEQUENCE. + * + * @param calendar The calendar to split + * @param componentName The name of the component to split (e.g. "VEVENT") + * + * @return A map of UID to [AssociatedComponents] */ fun associateByUid(calendar: Calendar, componentName: String): Map> { // get all components of type T (for instance: all VEVENTs) val all = calendar.getComponents(componentName) - TODO("ical4j 4.x") - // Note for VEVENT: UID is REQUIRED in RFC 5545 section 3.6.1, but optional in RFC 2445 section 4.6.1, // so it's possible that the Uid is null. - /*val byUid: Map> = all - .groupBy { it.uid?.value } + val byUid: Map> = all + .groupBy { it.uid.getOrNull()?.value } .mapValues { filterBySequence(it.value) } val result = mutableMapOf>() @@ -38,29 +43,27 @@ class CalendarUidSplitter { result[uid] = AssociatedComponents(mainVEvent, exceptions) } - return result*/ + return result } /** * Keeps only the events with the highest SEQUENCE (per RECURRENCE-ID). * - * @param events list of VEVENTs with the same UID, but different RECURRENCE-IDs (may be `null`) and SEQUENCEs + * @param events list of VEVENTs with the same UID, but different RECURRENCE-IDs (could be `null`) and SEQUENCEs * * @return same as input list, but each RECURRENCE-ID occurs only with the highest SEQUENCE */ @VisibleForTesting internal fun filterBySequence(events: List): List { - TODO("ical4j 4.x") - - // group by RECURRENCE-ID (may be null) - /*val byRecurId = events.groupBy { it.recurrenceId?.value }.values + // group by RECURRENCE-ID (could be null) + val byRecurId = events.groupBy { it.recurrenceId?.value }.values - // for every RECURRENCE-ID: keep only event with highest sequence + // for every RECURRENCE-ID: keep only event with the highest sequence val latest = byRecurId.map { sameUidAndRecurId -> sameUidAndRecurId.maxBy { it.sequence?.sequenceNo ?: 0 } } - return latest*/ + return latest } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt index 50840fdc..dea0903d 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt @@ -7,7 +7,6 @@ package at.bitfire.synctools.icalendar import at.bitfire.synctools.BuildConfig -import at.bitfire.synctools.exception.InvalidICalendarException import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyList @@ -17,6 +16,7 @@ import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.model.property.Sequence import net.fortuna.ical4j.model.property.Uid +import kotlin.jvm.optionals.getOrNull /** * The used version of ical4j. @@ -28,28 +28,19 @@ const val ical4jVersion = BuildConfig.version_ical4j // component access helpers fun componentListOf(vararg components: T): ComponentList = - TODO("ical4j 4.x") - /*ComponentList().apply { - addAll(components) - }*/ + ComponentList().addAll(components.toList()) fun propertyListOf(vararg properties: Property): PropertyList = - TODO("ical4j 4.x") - /*PropertyList().apply { - addAll(properties) - }*/ + PropertyList().addAll(properties.toList()) val CalendarComponent.uid: Uid? - get() = TODO("ical4j 4.x") - // getProperty(Property.UID) + get() = getProperty(Property.UID).getOrNull() val CalendarComponent.recurrenceId: RecurrenceId<*>? - get() = TODO("ical4j 4.x") - // getProperty(Property.RECURRENCE_ID) + get() = getProperty>(Property.RECURRENCE_ID).getOrNull() val CalendarComponent.sequence: Sequence? - get() = TODO("ical4j 4.x") - // getProperty(Property.SEQUENCE) + get() = getProperty(Property.SEQUENCE).getOrNull() fun VEvent.requireDtStart(): DtStart<*> = TODO("ical4j 4.x") diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt index e0745017..61cbf06a 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/CalendarUidSplitterTest.kt @@ -9,17 +9,16 @@ package at.bitfire.synctools.icalendar import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.ComponentList -import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.model.property.Sequence import net.fortuna.ical4j.model.property.Uid import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test +import java.time.Instant +import java.time.LocalDate -@Ignore("ical4j 4.x") class CalendarUidSplitterTest { @Test @@ -32,8 +31,7 @@ class CalendarUidSplitterTest { @Test fun testAssociatedVEventsByUid_ExceptionOnly_NoUid() { val exception = VEvent(propertyListOf( - TODO("ical4j 4.x") - //RecurrenceId("20250629T000000Z") + RecurrenceId("20250629T000000Z") )) val calendar = Calendar(componentListOf(exception)) val result = CalendarUidSplitter().associateByUid(calendar, Component.VEVENT) @@ -80,32 +78,28 @@ class CalendarUidSplitterTest { assertEquals(emptyList(), result) } - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun testFilterBySequence_MainAndExceptions_MultipleSequences() { val mainEvent1a = VEvent(propertyListOf(Sequence(1))) val mainEvent1b = VEvent(propertyListOf(Sequence(2))) val exception1a = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z"), + RecurrenceId("20250629T000000Z"), Sequence(1) )) val exception1b = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z"), + RecurrenceId("20250629T000000Z"), Sequence(2) )) val exception1c = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z"), + RecurrenceId("20250629T000000Z"), Sequence(3) )) val exception2a = VEvent(propertyListOf( - RecurrenceId(Date("20250629")) + RecurrenceId("20250629") // Sequence(0) )) val exception2b = VEvent(propertyListOf( - RecurrenceId(Date("20250629")), + RecurrenceId("20250629"), Sequence(1) )) val result = CalendarUidSplitter().filterBySequence( @@ -118,11 +112,11 @@ class CalendarUidSplitterTest { fun testFilterBySequence_MainAndExceptions_SingleSequence() { val mainEvent = VEvent(propertyListOf(Sequence(1))) val exception1 = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z"), + RecurrenceId("20250629T000000Z"), Sequence(1) )) val exception2 = VEvent(propertyListOf( - RecurrenceId(Date("20250629")) + RecurrenceId("20250629") // Sequence(0) )) val result = CalendarUidSplitter().filterBySequence( @@ -134,7 +128,7 @@ class CalendarUidSplitterTest { @Test fun testFilterBySequence_OnlyException_SingleSequence() { val exception = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z") + RecurrenceId("20250629T000000Z") )) val result = CalendarUidSplitter().filterBySequence(listOf(exception)) assertEquals(listOf(exception), result) @@ -143,30 +137,30 @@ class CalendarUidSplitterTest { @Test fun testFilterBySequence_OnlyExceptions_MultipleSequences() { val exception1a = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z"), + RecurrenceId("20250629T000000Z"), Sequence(1) )) val exception1b = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z"), + RecurrenceId("20250629T000000Z"), Sequence(2) )) val exception1c = VEvent(propertyListOf( - RecurrenceId("20250629T000000Z"), + RecurrenceId("20250629T000000Z"), Sequence(3) )) val exception2a = VEvent(propertyListOf( - RecurrenceId(Date("20250629")) + RecurrenceId("20250629") // Sequence(0) )) val exception2b = VEvent(propertyListOf( - RecurrenceId(Date("20250629")), + RecurrenceId("20250629"), Sequence(1) )) val result = CalendarUidSplitter().filterBySequence( listOf(exception1a, exception1c, exception1b, exception2a, exception2b) ) assertEquals(listOf(exception1c, exception2b), result) - }*/ + } @Test fun testFilterBySequence_OnlyMain_SingleSequence() { From 109a4cfb371dc567b65a851ec06b76eb56e92461 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 5 Mar 2026 19:07:28 +0100 Subject: [PATCH 04/24] [ical4j 4.x] Update `ICalendarGenerator` and `ICalendarParser` (#209) Migrate `ICalendarGenerator` and `ICalendarParser` to ical4j 4.x --- .../at/bitfire/ical4android/ICalendar.kt | 150 ++++++++++-------- .../synctools/icalendar/ICalendarGenerator.kt | 78 +++++---- .../synctools/icalendar/ICalendarParser.kt | 8 +- .../synctools/icalendar/Ical4jHelpers.kt | 20 ++- .../at/bitfire/ical4android/ICalendarTest.kt | 82 ++++++---- .../icalendar/ICalendarGeneratorTest.kt | 29 ++-- .../icalendar/ICalendarParserTest.kt | 2 - 7 files changed, 210 insertions(+), 159 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index 0a85a361..ff80ed45 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -10,18 +10,31 @@ import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME import at.bitfire.synctools.BuildConfig import at.bitfire.synctools.exception.InvalidICalendarException import at.bitfire.synctools.icalendar.ICalendarParser +import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.icalendar.validation.ICalPreprocessor import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.TemporalAdapter +import net.fortuna.ical4j.model.TemporalComparator +import net.fortuna.ical4j.model.component.Daylight import net.fortuna.ical4j.model.component.Observance +import net.fortuna.ical4j.model.component.Standard import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Related import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.validate.ValidationException import java.io.Reader +import java.time.ZonedDateTime +import java.time.temporal.Temporal +import java.time.temporal.TemporalAccessor +import java.time.temporal.TemporalAdjuster import java.util.LinkedList import java.util.UUID import java.util.logging.Level @@ -114,94 +127,91 @@ open class ICalendar { * - the last DAYLIGHT observance matching [start], and * - observances beginning after [start] * - * Additionally, TZURL properties are filtered. + * Additionally, all properties other than observances and TZID are dropped. * * @param originalTz time zone definition to minify * @param start start date for components (usually DTSTART); *null* if unknown * @return minified time zone definition */ - fun minifyVTimeZone(originalTz: VTimeZone, start: Date?): VTimeZone { + fun minifyVTimeZone(originalTz: VTimeZone, start: ZonedDateTime?): VTimeZone { + if (start == null) return originalTz + var newTz: VTimeZone? = null val keep = mutableSetOf() - TODO("ical4j 4.x") // Note: big method – maybe split? - /* - if (start != null) { - // find latest matching STANDARD/DAYLIGHT observances - var latestDaylight: Pair? = null - var latestStandard: Pair? = null - for (observance in originalTz.observances) { - val latest = observance.getLatestOnset(start) - - if (latest == null) // observance begins after "start", keep in any case - keep += observance - else - when (observance) { - is Standard -> - if (latestStandard == null || latest > latestStandard.first) - latestStandard = Pair(latest, observance) - is Daylight -> - if (latestDaylight == null || latest > latestDaylight.first) - latestDaylight = Pair(latest, observance) - } - } + // find latest matching STANDARD/DAYLIGHT observances + var latestDaylight: Pair? = null + var latestStandard: Pair? = null + for (observance in originalTz.observances) { + val latest = observance.getLatestOnset(start) - // keep latest STANDARD observance - latestStandard?.second?.let { keep += it } - - // Check latest DAYLIGHT for whether it can apply in the future. Otherwise, DST is not - // used in this time zone anymore and the DAYLIGHT component can be dropped completely. - latestDaylight?.second?.let { daylight -> - // check whether start time is in DST - if (latestStandard != null) { - val latestStandardOnset = latestStandard.second.getLatestOnset(start) - val latestDaylightOnset = daylight.getLatestOnset(start) - if (latestStandardOnset != null && latestDaylightOnset != null && latestDaylightOnset > latestStandardOnset) { - // we're currently in DST - keep += daylight - return@let - } + if (latest == null) // observance begins after "start", keep in any case + keep += observance + else + when (observance) { + is Standard -> + if (latestStandard == null || TemporalAdapter.isAfter(latest, latestStandard.first)) + latestStandard = Pair(latest, observance) + is Daylight -> + if (latestDaylight == null || TemporalAdapter.isAfter(latest, latestDaylight.first)) + latestDaylight = Pair(latest, observance) } + } - // check RRULEs - for (rRule in daylight.getProperties(Property.RRULE)) { - val nextDstOnset = rRule.recur.getNextDate(daylight.startDate.date, start) - if (nextDstOnset != null) { - // there will be a DST onset in the future -> keep DAYLIGHT - keep += daylight - return@let - } - } - // no RRULE, check whether there's an RDATE in the future - for (rDate in daylight.getProperties(Property.RDATE)) { - if (rDate.dates.any { it >= start }) { - // RDATE in the future - keep += daylight - return@let - } + // keep latest STANDARD observance + latestStandard?.second?.let { keep += it } + + // Check latest DAYLIGHT for whether it can apply in the future. Otherwise, DST is not + // used in this time zone anymore and the DAYLIGHT component can be dropped completely. + latestDaylight?.second?.let { daylight -> + // check whether start time is in DST + if (latestStandard != null) { + val latestStandardOnset = latestStandard.second.getLatestOnset(start) + val latestDaylightOnset = daylight.getLatestOnset(start) + if (latestStandardOnset != null && latestDaylightOnset != null && latestDaylightOnset > latestStandardOnset) { + // we're currently in DST + keep += daylight + return@let } } - // construct minified time zone that only contains the ID and relevant observances - val relevantProperties = PropertyList().apply { - add(originalTz.timeZoneId) - } - val relevantObservances = ComponentList().apply { - addAll(keep) + // Observance data is using LocalDateTime. Drop time zone information for comparisons. + val startLocal = start.toLocalDateTime() + + // check RRULEs + for (rRule in daylight.getProperties>(Property.RRULE)) { + val nextDstOnset = rRule.recur.getNextDate(daylight.startDate.date, startLocal) + if (nextDstOnset != null) { + // there will be a DST onset in the future -> keep DAYLIGHT + keep += daylight + return@let + } } - newTz = VTimeZone(relevantProperties, relevantObservances) - - // validate minified timezone - try { - newTz.validate() - } catch (e: ValidationException) { - // This should never happen! - logger.log(Level.WARNING, "Minified timezone is invalid, using original one", e) - newTz = null + // no RRULE, check whether there's an RDATE in the future + for (rDate in daylight.getProperties>(Property.RDATE)) { + if (rDate.dates.any { !TemporalAdapter.isBefore(it, startLocal) }) { + // RDATE in the future + keep += daylight + return@let + } } - }*/ + } + + // construct minified time zone that only contains the ID and relevant observances + val relevantProperties = propertyListOf(originalTz.timeZoneId) + val relevantObservances = ComponentList(keep.toList()) + newTz = VTimeZone(relevantProperties, relevantObservances) + + // validate minified timezone + try { + newTz.validate() + } catch (e: ValidationException) { + // This should never happen! + logger.log(Level.WARNING, "Minified timezone is invalid, using original one", e) + newTz = null + } // use original time zone if we couldn't calculate a minified one return newTz ?: originalTz diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt index c928ae42..f92251d6 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt @@ -9,16 +9,20 @@ package at.bitfire.synctools.icalendar import at.bitfire.ical4android.ICalendar import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.model.Calendar -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.TimeZone +import net.fortuna.ical4j.model.TemporalAdapter +import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.CalendarComponent import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DateProperty -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.Version +import net.fortuna.ical4j.model.property.immutable.ImmutableVersion import java.io.Writer +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.Temporal import javax.annotation.WillNotClose /** @@ -34,61 +38,73 @@ class ICalendarGenerator { * @param to stream that the iCalendar is written to */ fun write(event: AssociatedComponents<*>, @WillNotClose to: Writer) { - TODO("ical4j 4.x") - /*val ical = Calendar() - ical.properties += Version.VERSION_2_0 + val ical = Calendar() + ical += ImmutableVersion.VERSION_2_0 // add PRODID if (event.prodId != null) - ical.properties += event.prodId + ical += event.prodId // keep record of used timezones and earliest DTSTART to generate minified VTIMEZONEs - var earliestStart: Date? = null - val usedTimeZones = mutableSetOf() + var earliestStart: Temporal? = null + val usedTimeZones = mutableSetOf() // add main event if (event.main != null) { - ical.components += event.main + ical += event.main - earliestStart = event.main.getProperty(Property.DTSTART)?.date + earliestStart = event.main.dtStart()?.date usedTimeZones += timeZonesOf(event.main) } // recurrence exceptions for (exception in event.exceptions) { - ical.components += exception + ical += exception - exception.getProperty(Property.DTSTART)?.date?.let { start -> - if (earliestStart == null || start <= earliestStart) + exception.dtStart()?.date?.let { start -> + if (earliestStart == null || TemporalAdapter.isBefore(start, earliestStart)) earliestStart = start } usedTimeZones += timeZonesOf(exception) } // add VTIMEZONE components - for (tz in usedTimeZones) - ical.components += ICalendar.minifyVTimeZone(tz.vTimeZone, earliestStart) + val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() + for (tz in usedTimeZones) { + val vTimeZone = tzReg.getTimeZone(tz.id).vTimeZone + val minifiedVTimeZone = ICalendar.minifyVTimeZone(vTimeZone, earliestStart.toZonedDateTime(tz)) + ical += minifiedVTimeZone + } - CalendarOutputter(false).output(ical, to)*/ + CalendarOutputter(false).output(ical, to) } - private fun timeZonesOf(component: CalendarComponent): Set { - TODO("ical4j 4.x") - /*val timeZones = mutableSetOf() + private fun timeZonesOf(component: CalendarComponent): Set { + val timeZones = mutableSetOf() // properties - timeZones += component.properties - .filterIsInstance() - .mapNotNull { (it.date as? DateTime)?.timeZone } + timeZones += component.propertyList.all + .filterIsInstance>() + .mapNotNull { (it.date as? ZonedDateTime)?.zone } // properties of subcomponents (alarms) if (component is VEvent) - for (subcomponent in component.components) - timeZones += subcomponent.properties - .filterIsInstance() - .mapNotNull { (it.date as? DateTime)?.timeZone } + for (subcomponent in component.componentList.all) + timeZones += subcomponent.propertyList.all + .filterIsInstance>() + .mapNotNull { (it.date as? ZonedDateTime)?.zone } - return timeZones*/ + return timeZones } + private fun Temporal?.toZonedDateTime(zoneId: ZoneId): ZonedDateTime? { + return when (this) { + is LocalDate -> this.atStartOfDay().atZone(zoneId) + is LocalDateTime -> this.atZone(zoneId) + is OffsetDateTime -> this.atZoneSameInstant(zoneId) + is Instant -> this.atZone(zoneId) + is ZonedDateTime -> this + else -> null + } + } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt index f2bdd8de..a55232cd 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarParser.kt @@ -42,17 +42,15 @@ class ICalendarParser( * @throws InvalidICalendarException when the resource is can't be parsed */ fun parse(@WillNotClose reader: Reader): Calendar { - TODO("ical4j 4.x") - // preprocess stream to work around problems that prevent parsing and thus can't be fixed later - /*val preprocessed = preprocessor.preprocessStream(reader) + val preprocessed = preprocessor.preprocessStream(reader) // parse stream, ignoring invalid properties (if possible) val calendar: Calendar try { calendar = CalendarBuilder( /* parser = */ CalendarParserFactory.getInstance().get(), - /* contentHandlerContext = */ ContentHandlerContext().withSupressInvalidProperties(/* supressInvalidProperties = */ true), + /* contentHandlerContext = */ ContentHandlerContext().withSuppressInvalidProperties(true), /* tzRegistry = */ TimeZoneRegistryFactory.getInstance().createRegistry() ).build(preprocessed) } catch(e: ParserException) { @@ -68,7 +66,7 @@ class ICalendarParser( logger.log(Level.WARNING, "Couldn't pre-process iCalendar", e) } - return calendar*/ + return calendar } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt index dea0903d..24e98d45 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt @@ -7,8 +7,11 @@ package at.bitfire.synctools.icalendar import at.bitfire.synctools.BuildConfig +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.ComponentContainer import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.PropertyContainer import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.component.CalendarComponent import net.fortuna.ical4j.model.component.VEvent @@ -16,6 +19,7 @@ import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.model.property.Sequence import net.fortuna.ical4j.model.property.Uid +import java.time.temporal.Temporal import kotlin.jvm.optionals.getOrNull /** @@ -28,10 +32,10 @@ const val ical4jVersion = BuildConfig.version_ical4j // component access helpers fun componentListOf(vararg components: T): ComponentList = - ComponentList().addAll(components.toList()) + ComponentList(components.toList()) fun propertyListOf(vararg properties: Property): PropertyList = - PropertyList().addAll(properties.toList()) + PropertyList(properties.toList()) val CalendarComponent.uid: Uid? get() = getProperty(Property.UID).getOrNull() @@ -42,6 +46,18 @@ val CalendarComponent.recurrenceId: RecurrenceId<*>? val CalendarComponent.sequence: Sequence? get() = getProperty(Property.SEQUENCE).getOrNull() +fun CalendarComponent.dtStart(): DtStart? { + return getProperty>(Property.DTSTART).getOrNull() +} + fun VEvent.requireDtStart(): DtStart<*> = TODO("ical4j 4.x") // startDate ?: throw InvalidICalendarException("Missing DTSTART in VEVENT") + +operator fun PropertyContainer.plusAssign(property: Property) { + add(property) +} + +operator fun ComponentContainer.plusAssign(component: T) { + add>(component) +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt index 0399f7ec..f6c40a77 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -10,6 +10,7 @@ import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VTimeZone @@ -19,18 +20,16 @@ import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due import net.fortuna.ical4j.util.TimeZones -import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Ignore import org.junit.Test import java.io.StringReader -import java.time.Duration -import java.time.Period +import java.time.LocalDateTime +import java.time.ZonedDateTime import java.util.Date -@Ignore("ical4j 4.x") class ICalendarTest { // UTC timezone @@ -52,14 +51,14 @@ class ICalendarTest { private fun readTimeZone(fileName: String): VTimeZone { - TODO("ical4j 4.x") - /*javaClass.classLoader!!.getResourceAsStream("tz/$fileName").use { tzStream -> + javaClass.classLoader!!.getResourceAsStream("tz/$fileName").use { tzStream -> val cal = CalendarBuilder().build(tzStream) - val vTimeZone = cal.getComponent(Component.VTIMEZONE) as VTimeZone + val vTimeZone = cal.getComponent(Component.VTIMEZONE).get() return vTimeZone - }*/ + } } + @Ignore("ical4j 4.x") @Test fun testFromReader_calendarProperties() { val calendar = ICalendar.fromReader( @@ -80,6 +79,7 @@ class ICalendarTest { assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).value)*/ } + @Ignore("ical4j 4.x") @Test fun testFromReader_invalidProperty() { // The GEO property is invalid and should be ignored. @@ -108,9 +108,10 @@ class ICalendarTest { // DATE-TIME values in UTC are usually noted with ...Z and don't have a VTIMEZONE, // but it is allowed to write them as TZID=Etc/UTC. assertEquals(1, vtzUTC.observances.size) - ICalendar.minifyVTimeZone(vtzUTC, net.fortuna.ical4j.model.Date("20200612")).let { minified -> - assertEquals(1, minified.observances.size) - } + + val minified = ICalendar.minifyVTimeZone(vtzUTC, vtzUTC.zonedDateTime("2020-06-12T00:00")) + + assertEquals(1, minified.observances.size) } @Test @@ -118,14 +119,14 @@ class ICalendarTest { // Remove obsolete observances when DST is used. assertEquals(6, vtzVienna.observances.size) // By default, the earliest observance is in 1893. We can drop that for events in 2020. - assertEquals(DateTime("18930401T000000"), vtzVienna.observances.sortedBy { it.startDate.date }.first().startDate.date) - ICalendar.minifyVTimeZone(vtzVienna, net.fortuna.ical4j.model.Date("20200101")).let { minified -> - Assert.assertEquals(2, minified.observances.size) - // now earliest observance for DAYLIGHT/STANDARD is 1981/1996 - assertEquals(DateTime("19961027T030000"), minified.observances[0].startDate.date) - assertEquals(DateTime("19810329T020000"), minified.observances[1].startDate.date) - } + assertEquals(LocalDateTime.parse("1893-04-01T00:00:00"), vtzVienna.observances.minOfOrNull { it.startDate.date }) + + val minified = ICalendar.minifyVTimeZone(vtzVienna, vtzVienna.zonedDateTime("2020-01-01")) + assertEquals(2, minified.observances.size) + // now earliest observance for STANDARD/DAYLIGHT is 1996/1981 + assertEquals(LocalDateTime.parse("1996-10-27T03:00:00"), minified.observances[0].startDate.date) + assertEquals(LocalDateTime.parse("1981-03-29T02:00:00"), minified.observances[1].startDate.date) } @Test @@ -133,26 +134,36 @@ class ICalendarTest { // Remove obsolete observances when DST is not used. Mogadishu had several time zone changes, // but now there is a simple offest without DST. assertEquals(4, vtzMogadishu.observances.size) - ICalendar.minifyVTimeZone(vtzMogadishu, net.fortuna.ical4j.model.Date("19611001")).let { minified -> - assertEquals(1, minified.observances.size) - } + + val minified = ICalendar.minifyVTimeZone(vtzMogadishu, vtzMogadishu.zonedDateTime("1961-10-01")) + + assertEquals(1, minified.observances.size) } @Test fun testMinifyVTimezone_keepFutureObservances() { // Keep future observances. - ICalendar.minifyVTimeZone(vtzVienna, net.fortuna.ical4j.model.Date("19751001")).let { minified -> - Assert.assertEquals(4, minified.observances.size) - assertEquals(DateTime("19161001T010000"), minified.observances[2].startDate.date) - assertEquals(DateTime("19160430T230000"), minified.observances[3].startDate.date) + ICalendar.minifyVTimeZone(vtzVienna, vtzVienna.zonedDateTime("1975-10-01")).let { minified -> + val sortedStartDates = minified.observances + .map { it.startDate.date } + .sorted() + .map { it.toString() } + + assertEquals( + listOf("1916-04-30T23:00", "1916-10-01T01:00", "1981-03-29T02:00", "1996-10-27T03:00"), + sortedStartDates + ) } - ICalendar.minifyVTimeZone(vtzKarachi, net.fortuna.ical4j.model.Date("19611001")).let { minified -> + + ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("1961-10-01")).let { minified -> assertEquals(4, minified.observances.size) } - ICalendar.minifyVTimeZone(vtzKarachi, net.fortuna.ical4j.model.Date("19751001")).let { minified -> + + ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("1975-10-01")).let { minified -> assertEquals(3, minified.observances.size) } - ICalendar.minifyVTimeZone(vtzMogadishu, net.fortuna.ical4j.model.Date("19311001")).let { minified -> + + ICalendar.minifyVTimeZone(vtzMogadishu, vtzMogadishu.zonedDateTime("1931-10-01")).let { minified -> assertEquals(3, minified.observances.size) } } @@ -160,7 +171,7 @@ class ICalendarTest { @Test fun testMinifyVTimezone_keepDstWhenStartInDst() { // Keep DST when there are no obsolete observances, but start time is in DST. - ICalendar.minifyVTimeZone(vtzKarachi, net.fortuna.ical4j.model.Date("20091031")).let { minified -> + ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("2009-10-31")).let { minified -> assertEquals(2, minified.observances.size) } } @@ -168,12 +179,13 @@ class ICalendarTest { @Test fun testMinifyVTimezone_removeDstWhenNotUsedAnymore() { // Remove obsolete observances (including DST) when DST is not used anymore. - ICalendar.minifyVTimeZone(vtzKarachi, net.fortuna.ical4j.model.Date("201001001")).let { minified -> + ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("2010-01-01")).let { minified -> assertEquals(1, minified.observances.size) } } + @Ignore("ical4j 4.x") @Test fun testTimezoneDefToTzId_Valid() { assertEquals( @@ -204,6 +216,7 @@ class ICalendarTest { ) } + @Ignore("ical4j 4.x") @Test fun testTimezoneDefToTzId_Invalid() { // invalid time zone @@ -221,10 +234,6 @@ class ICalendarTest { } - init { - TODO("ical4j 4.x") - } - /*@Test fun testVAlarmToMin_TriggerDuration_Negative() { // TRIGGER;REL=START:-P1DT1H1M29S @@ -359,4 +368,9 @@ class ICalendarTest { assertEquals(8*24*60, min) }*/ +} + +private fun VTimeZone.zonedDateTime(text: String): ZonedDateTime { + val dateTimeText = if ('T' in text) text else "${text}T00:00:00" + return LocalDateTime.parse(dateTimeText).atZone(TimeZone(this).toZoneId()) } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt index 4c7307cc..93a33373 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt @@ -17,20 +17,20 @@ import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.model.property.Uid -import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test import java.io.StringWriter import java.time.Duration +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZonedDateTime +import java.time.temporal.Temporal -@Ignore("ical4j 4.x") class ICalendarGeneratorTest { private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzBerlin = tzRegistry.getTimeZone("Europe/Berlin")!! - private val tzLondon = tzRegistry.getTimeZone("Europe/London")!! - private val tzUTC = tzRegistry.getTimeZone(TimeZones.UTC_ID)!! + private val tzBerlin = tzRegistry.getTimeZone("Europe/Berlin").toZoneId() + private val tzLondon = tzRegistry.getTimeZone("Europe/London").toZoneId() private val userAgent = ProdId("TestUA/1.0") private val writer = ICalendarGenerator() @@ -38,29 +38,28 @@ class ICalendarGeneratorTest { @Test fun `Write event with exceptions and various timezones`() { val iCal = StringWriter() - TODO("ical4j 4.x") - /*writer.write(AssociatedEvents( + writer.write(AssociatedEvents( main = VEvent(propertyListOf( Uid("SAMPLEUID"), - DtStart("20190101T100000", tzBerlin), - DtEnd("20190101T160000Z"), + DtStart(ZonedDateTime.of(LocalDateTime.parse("2019-01-01T10:00:00"), tzBerlin)), + DtEnd(Instant.parse("2019-01-01T16:00:00Z")), DtStamp("20251028T185101Z"), - RRule("FREQ=DAILY;COUNT=5") + RRule("FREQ=DAILY;COUNT=5") ), ComponentList(listOf( VAlarm(Duration.ofHours(-1)) ))), exceptions = listOf( VEvent(propertyListOf( Uid("SAMPLEUID"), - RecurrenceId("20190102T100000", tzBerlin), - DtStart("20190101T110000", tzLondon), - DtEnd("20190101T170000Z"), + RecurrenceId(ZonedDateTime.of(LocalDateTime.parse("2019-01-02T10:00:00"), tzBerlin)), + DtStart(ZonedDateTime.of(LocalDateTime.parse("2019-01-01T11:00:00"), tzLondon)), + DtEnd(Instant.parse("2019-01-01T17:00:00Z")), DtStamp("20251028T185101Z") )) ), prodId = userAgent - ), iCal)*/ + ), iCal) assertEquals("BEGIN:VCALENDAR\r\n" + "VERSION:2.0\r\n" + diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt index c007677a..6dedfd13 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarParserTest.kt @@ -15,13 +15,11 @@ import io.mockk.junit4.MockKRule import io.mockk.slot import io.mockk.verify import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.io.Reader import java.io.StringReader -@Ignore("ical4j 4.x") class ICalendarParserTest { @get:Rule From 58bc4e7aa15d1521efcaf23e406f4d8edde50d1b Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 10 Mar 2026 08:38:46 +0100 Subject: [PATCH 05/24] [ical4j 4.x] Update event validators (#210) * Handle empty timezones in `AndroidCompatTimeZoneRegistry` (#203) * Handle empty timezones Signed-off-by: Arnau Mora * Just check for empty timezones Signed-off-by: Arnau Mora * Add clarifying comment Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora * Update minSdkVersion to 24 and remove deprecated code (#204) * Update minSdkVersion * - Replace deprecated `closeCompat()` with standard `close()` - Remove compatibility method `closeCompat()` from `MiscUtils` --------- Co-authored-by: cketti * [ical4j 4.x] Update event validators Signed-off-by: Arnau Mora * Remove UTC validation rule Signed-off-by: Arnau Mora * Fix test Signed-off-by: Arnau Mora * Implement helper Signed-off-by: Arnau Mora * Throw original exception Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner Co-authored-by: cketti --- .../synctools/icalendar/Ical4jHelpers.kt | 6 +++--- .../icalendar/validation/ICalPreprocessor.kt | 19 +++++++------------ .../validation/ICalPreprocessorTest.kt | 19 +++++++++++++------ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt index 24e98d45..3670fb0e 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt @@ -7,6 +7,7 @@ package at.bitfire.synctools.icalendar import at.bitfire.synctools.BuildConfig +import at.bitfire.synctools.exception.InvalidICalendarException import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.ComponentContainer import net.fortuna.ical4j.model.ComponentList @@ -50,9 +51,8 @@ fun CalendarComponent.dtStart(): DtStart? { return getProperty>(Property.DTSTART).getOrNull() } -fun VEvent.requireDtStart(): DtStart<*> = - TODO("ical4j 4.x") - // startDate ?: throw InvalidICalendarException("Missing DTSTART in VEVENT") +fun VEvent.requireDtStart(): DtStart = + getProperty>(Property.DTSTART).getOrNull() ?: throw InvalidICalendarException("Missing DTSTART in VEVENT") operator fun PropertyContainer.plusAssign(property: Property) { add(property) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt index 1f7fc029..b7ec7ade 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt @@ -12,6 +12,7 @@ import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.transform.compliance.DateListPropertyRule import net.fortuna.ical4j.transform.compliance.DatePropertyRule +import net.fortuna.ical4j.transform.compliance.Rfc5545PropertyRule import java.io.BufferedReader import java.io.Reader import java.util.logging.Logger @@ -20,7 +21,6 @@ import javax.annotation.WillNotClose /** * Applies some rules to increase compatibility of parsed (incoming) iCalendars: * - * - [CreatedPropertyRule] to make sure CREATED is UTC * - [DatePropertyRule] and [DateListPropertyRule] to rename Outlook-specific TZID parameters * (like "W. Europe Standard Time" to an Android-friendly name like "Europe/Vienna") */ @@ -30,9 +30,6 @@ class ICalPreprocessor { get() = Logger.getLogger(javaClass.name) private val propertyRules = arrayOf( - TODO("ical4j 4.x"), - //CreatedPropertyRule(), // make sure CREATED is UTC - DatePropertyRule(), // These two rules also replace VTIMEZONEs of the iCalendar ... DateListPropertyRule() // ... by the ical4j VTIMEZONE with the same TZID! ) @@ -96,24 +93,22 @@ class ICalPreprocessor { * @param calendar the calendar object that is going to be modified */ fun preprocessCalendar(calendar: Calendar) { - TODO("ical4j 4.x") - /*for (component in calendar.components) - for (property in component.properties) - applyRules(property)*/ + for (component in calendar.componentList.all) + for (property in component.propertyList.all) + applyRules(property) } @Suppress("UNCHECKED_CAST") private fun applyRules(property: Property) { - TODO("ical4j 4.x") - /*propertyRules + propertyRules .filter { rule -> rule.supportedType.isAssignableFrom(property::class.java) } .forEach { rule -> val beforeStr = property.toString() - (rule as Rfc5545PropertyRule).applyTo(property) + (rule as Rfc5545PropertyRule).apply(property) val afterStr = property.toString() if (beforeStr != afterStr) logger.info("${rule.javaClass.name}: $beforeStr -> $afterStr") - }*/ + } } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt index 4500e004..51cb51eb 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessorTest.kt @@ -6,6 +6,7 @@ package at.bitfire.synctools.icalendar.validation +import at.bitfire.synctools.icalendar.requireDtStart import com.google.common.io.CharStreams import io.mockk.junit4.MockKRule import io.mockk.mockkObject @@ -13,19 +14,20 @@ import io.mockk.spyk import io.mockk.verify import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.parameter.TzId import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Rule import org.junit.Test import java.io.InputStreamReader import java.io.Reader import java.io.StringReader import java.io.Writer +import java.time.temporal.Temporal import java.util.UUID -@Ignore("ical4j 4.x") class ICalPreprocessorTest { @get:Rule @@ -56,12 +58,17 @@ class ICalPreprocessorTest { javaClass.getResourceAsStream("/events/outlook1.ics").use { stream -> val reader = InputStreamReader(stream, Charsets.UTF_8) val calendar = CalendarBuilder().build(reader) - TODO("ical4j 4.x") - /*val vEvent = calendar.getComponent(Component.VEVENT) as VEvent + val vEvent = calendar.getComponent(Component.VEVENT).get() - assertEquals("W. Europe Standard Time", vEvent.startDate.timeZone.id) + assertEquals( + "W. Europe Standard Time", + vEvent.requireDtStart().getRequiredParameter(Parameter.TZID).value + ) processor.preprocessCalendar(calendar) - assertEquals("Europe/Vienna", vEvent.startDate.timeZone.id)*/ + assertEquals( + "Europe/Vienna", + vEvent.requireDtStart().getRequiredParameter(Parameter.TZID).value + ) } } From 416036d406fc0e0ab3741f1a0f92a785bd8e623f Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 10 Mar 2026 09:39:17 +0100 Subject: [PATCH 06/24] [ical4j 4.x] Update iCalendar (#215) * Update ICalendar.vAlarmToMin() * Update ICalendar.timezoneDefToTzId() * Update ICalendar.fromReader() # Conflicts: # lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt * Remove CreatedPropertyRule  Conflicts:  lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt * Typo --- .../at/bitfire/ical4android/ICalendar.kt | 69 ++++++++------- .../icalendar/validation/ICalPreprocessor.kt | 1 + .../at/bitfire/ical4android/ICalendarTest.kt | 83 +++++++++---------- 3 files changed, 79 insertions(+), 74 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index ff80ed45..dcd7ffd4 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -12,33 +12,38 @@ import at.bitfire.synctools.exception.InvalidICalendarException import at.bitfire.synctools.icalendar.ICalendarParser import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.icalendar.validation.ICalPreprocessor +import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.ComponentList -import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.TemporalAdapter -import net.fortuna.ical4j.model.TemporalComparator import net.fortuna.ical4j.model.component.Daylight import net.fortuna.ical4j.model.component.Observance import net.fortuna.ical4j.model.component.Standard import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.Trigger import net.fortuna.ical4j.validate.ValidationException import java.io.Reader +import java.io.StringReader +import java.time.Duration +import java.time.Period import java.time.ZonedDateTime import java.time.temporal.Temporal -import java.time.temporal.TemporalAccessor -import java.time.temporal.TemporalAdjuster import java.util.LinkedList import java.util.UUID import java.util.logging.Level import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull open class ICalendar { @@ -101,17 +106,16 @@ open class ICalendar { // fill calendar properties properties?.let { - TODO("ical4j 4.x") - /*calendar.getProperty(CALENDAR_NAME)?.let { calName -> + calendar.getProperty(CALENDAR_NAME).getOrNull()?.let { calName -> properties[CALENDAR_NAME] = calName.value } - calendar.getProperty(Color.PROPERTY_NAME)?.let { calColor -> + calendar.getProperty(Color.PROPERTY_NAME).getOrNull()?.let { calColor -> properties[Color.PROPERTY_NAME] = calColor.value } - calendar.getProperty(CALENDAR_COLOR)?.let { calColor -> + calendar.getProperty(CALENDAR_COLOR).getOrNull()?.let { calColor -> properties[CALENDAR_COLOR] = calColor.value - }*/ + } } return calendar @@ -223,15 +227,14 @@ open class ICalendar { * @return time zone id (TZID) if VTIMEZONE contains a TZID, null otherwise */ fun timezoneDefToTzId(timezoneDef: String): String? { - TODO("ical4j 4.x") - /*try { + try { val builder = CalendarBuilder() val cal = builder.build(StringReader(timezoneDef)) - val timezone = cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone? + val timezone = cal.getComponent(VTimeZone.VTIMEZONE).getOrNull() timezone?.timeZoneId?.let { return it.value } } catch (e: ParserException) { logger.log(Level.SEVERE, "Can't understand time zone definition", e) - }*/ + } return null } @@ -261,7 +264,7 @@ open class ICalendar { // misc. iCalendar helpers /** - * Calculates the minutes before/after an event/task a given alarm occurs. + * Calculates the minutes before/after an event/task to know when a given alarm occurs. * * @param alarm the alarm to calculate the minutes from * @param refStart reference `DTSTART` from the calendar component @@ -286,34 +289,34 @@ open class ICalendar { refDuration: net.fortuna.ical4j.model.property.Duration?, allowRelEnd: Boolean ): Pair? { - val trigger = alarm.trigger ?: return null + val trigger = alarm.getProperty(Property.TRIGGER).getOrNull() ?: return null - TODO("ical4j 4.x") // Note: big method – maybe split? - /*val minutes: Int // minutes before/after the event - var related = trigger.getParameter(Parameter.RELATED) ?: Related.START + val minutes: Int // minutes before/after the event + var related: Related = trigger.getParameter(Parameter.RELATED).getOrNull() ?: Related.START - // event/task start time - val start: java.util.Date? = refStart?.date - var end: java.util.Date? = refEnd?.date + // event/task start/end time + val start: Temporal? = refStart?.date + var end: Temporal? = refEnd?.date // event/task end time - if (end == null && start != null) { - val duration = refDuration?.duration - if (duration != null) - end = java.util.Date.from(start.toInstant() + duration) - } + if (end == null && start != null) + end = when (val refDur = refDuration?.duration) { + is Duration -> start + refDur + is Period -> start + Duration.between(start, start + refDur) + else -> null + } // event/task duration val duration: Duration? = if (start != null && end != null) - Duration.between(start.toInstant(), end.toInstant()) + Duration.between(start, end) else null val triggerDur = trigger.duration - val triggerTime = trigger.dateTime + val triggerTime = trigger.date if (triggerDur != null) { // TRIGGER value is a DURATION. Important: @@ -323,9 +326,11 @@ open class ICalendar { var millisBefore = when (triggerDur) { is Duration -> -triggerDur.toMillis() - is Period -> // TODO: Take time zones into account (will probably be possible with ical4j 4.x). + is Period -> { + // TODO: Take time zones into account (will probably be possible with ical4j 4.x). // For instance, an alarm one day before the DST change should be 23/25 hours before the event. - -triggerDur.days.toLong()*24*3600000 // months and years are not used in DURATION values; weeks are calculated to days + -Duration.ofDays(triggerDur.days.toLong()).toMillis() // months and years are not used in DURATION values; weeks are calculated to days + } else -> throw AssertionError("triggerDur must be Duration or Period") } @@ -343,14 +348,14 @@ open class ICalendar { } else if (triggerTime != null && start != null) { // TRIGGER value is a DATE-TIME, calculate minutes from start time related = Related.START - minutes = Duration.between(triggerTime.toInstant(), start.toInstant()).toMinutes().toInt() + minutes = Duration.between(triggerTime, start).toMinutes().toInt() } else { logger.log(Level.WARNING, "VALARM TRIGGER type is not DURATION or DATE-TIME (requires event DTSTART for Android), ignoring alarm", alarm) return null } - return Pair(related, minutes)*/ + return Pair(related, minutes) } } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt index b7ec7ade..d030c782 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/validation/ICalPreprocessor.kt @@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting import com.google.common.io.CharSource import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.component.CalendarComponent import net.fortuna.ical4j.transform.compliance.DateListPropertyRule import net.fortuna.ical4j.transform.compliance.DatePropertyRule import net.fortuna.ical4j.transform.compliance.Rfc5545PropertyRule diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt index f6c40a77..6ea598bf 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -8,8 +8,8 @@ package at.bitfire.ical4android import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.Component -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.Property.TRIGGER import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm @@ -19,16 +19,19 @@ import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.Trigger import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import java.io.StringReader import java.time.LocalDateTime +import java.time.Period import java.time.ZonedDateTime -import java.util.Date +import java.time.temporal.Temporal +import kotlin.jvm.optionals.getOrNull class ICalendarTest { @@ -47,7 +50,7 @@ class ICalendarTest { private val vtzMogadishu = readTimeZone("Mogadishu.ics") // current time stamp - private val currentTime = Date().time + private val currentTime = ZonedDateTime.now() private fun readTimeZone(fileName: String): VTimeZone { @@ -58,7 +61,7 @@ class ICalendarTest { } } - @Ignore("ical4j 4.x") + @Test fun testFromReader_calendarProperties() { val calendar = ICalendar.fromReader( @@ -73,13 +76,11 @@ class ICalendarTest { "END:VCALENDAR" ) ) - TODO("ical4j 4.x") - /*assertEquals("Some Calendar", calendar.getProperty(ICalendar.CALENDAR_NAME).value) - assertEquals("darkred", calendar.getProperty(Color.PROPERTY_NAME).value) - assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).value)*/ + assertEquals("Some Calendar", calendar.getProperty(ICalendar.CALENDAR_NAME).getOrNull()?.value) + assertEquals("darkred", calendar.getProperty(Color.PROPERTY_NAME).getOrNull()?.value) + assertEquals("#123456", calendar.getProperty(ICalendar.CALENDAR_COLOR).getOrNull()?.value) } - @Ignore("ical4j 4.x") @Test fun testFromReader_invalidProperty() { // The GEO property is invalid and should be ignored. @@ -185,7 +186,6 @@ class ICalendarTest { } - @Ignore("ical4j 4.x") @Test fun testTimezoneDefToTzId_Valid() { assertEquals( @@ -216,7 +216,6 @@ class ICalendarTest { ) } - @Ignore("ical4j 4.x") @Test fun testTimezoneDefToTzId_Invalid() { // invalid time zone @@ -234,12 +233,12 @@ class ICalendarTest { } - /*@Test + @Test fun testVAlarmToMin_TriggerDuration_Negative() { // TRIGGER;REL=START:-P1DT1H1M29S val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("-P1DT1H1M29S")), - DtStart(), null, null, false + VAlarm(Duration("-P1DT1H1M29S").duration), + DtStart(), null, null, false )!! assertEquals(Related.START, ref) assertEquals(60 * 24 + 60 + 1, min) @@ -249,8 +248,8 @@ class ICalendarTest { fun testVAlarmToMin_TriggerDuration_OnlySeconds() { // TRIGGER;REL=START:-PT3600S val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("-PT3600S")), - DtStart(), null, null, false + VAlarm(Duration("-PT3600S").duration), + DtStart(), null, null, false )!! assertEquals(Related.START, ref) assertEquals(60, min) @@ -260,8 +259,8 @@ class ICalendarTest { fun testVAlarmToMin_TriggerDuration_Positive() { // TRIGGER;REL=START:P1DT1H1M30S (alarm *after* start) val (ref, min) = ICalendar.vAlarmToMin( - VAlarm(Duration.parse("P1DT1H1M30S")), - DtStart(), null, null, false + VAlarm(Duration("P1DT1H1M30S").duration), + DtStart(), null, null, false )!! assertEquals(Related.START, ref) assertEquals(-(60 * 24 + 60 + 1), min) @@ -270,9 +269,9 @@ class ICalendarTest { @Test fun testVAlarmToMin_TriggerDuration_RelEndAllowed() { // TRIGGER;REL=END:-P1DT1H1M30S (caller accepts Related.END) - val alarm = VAlarm(Duration.parse("-P1DT1H1M30S")) - alarm.trigger.parameters.add(Related.END) - val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(), null, null, true)!! + val alarm = VAlarm(Duration("-P1DT1H1M30S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) + val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(), null, null, true)!! assertEquals(Related.END, ref) assertEquals(60 * 24 + 60 + 1, min) } @@ -280,12 +279,12 @@ class ICalendarTest { @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed() { // event with TRIGGER;REL=END:-PT30S (caller doesn't accept Related.END) - val alarm = VAlarm(Duration.parse("-PT65S")) - alarm.trigger.parameters.add(Related.END) + val alarm = VAlarm(Duration("-PT65S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) val (ref, min) = ICalendar.vAlarmToMin( alarm, - DtStart(DateTime(currentTime)), - DtEnd(DateTime(currentTime + 180 * 1000)), // 180 sec later + DtStart(currentTime), + DtEnd(currentTime.plusSeconds(180)), // 180 sec later null, false )!! @@ -297,40 +296,40 @@ class ICalendarTest { @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed_NoDtStart() { // event with TRIGGER;REL=END:-PT30S (caller doesn't accept Related.END) - val alarm = VAlarm(Duration.parse("-PT65S")) - alarm.trigger.parameters.add(Related.END) - assertNull(ICalendar.vAlarmToMin(alarm, DtStart(), DtEnd(DateTime(currentTime)), null, false)) + val alarm = VAlarm(Duration("-PT65S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) + assertNull(ICalendar.vAlarmToMin(alarm, DtStart(), DtEnd(currentTime), null, false)) } @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed_NoDuration() { // event with TRIGGER;REL=END:-PT30S (caller doesn't accept Related.END) - val alarm = VAlarm(Duration.parse("-PT65S")) - alarm.trigger.parameters.add(Related.END) - assertNull(ICalendar.vAlarmToMin(alarm, DtStart(DateTime(currentTime)), null, null, false)) + val alarm = VAlarm(Duration("-PT65S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) + assertNull(ICalendar.vAlarmToMin(alarm, DtStart(currentTime), null, null, false)) } @Test fun testVAlarmToMin_TriggerDuration_RelEndNotAllowed_AfterEnd() { // task with TRIGGER;REL=END:-P1DT1H1M30S (caller doesn't accept Related.END; alarm *after* end) - val alarm = VAlarm(Duration.parse("P1DT1H1M30S")) - alarm.trigger.parameters.add(Related.END) + val alarm = VAlarm(Duration("P1DT1H1M30S").duration) + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) val (ref, min) = ICalendar.vAlarmToMin( alarm, - DtStart(DateTime(currentTime)), - Due(DateTime(currentTime + 90 * 1000)), // 90 sec (should be rounded down to 1 min) later + DtStart(currentTime), + Due(currentTime.plusSeconds(90)), // 90 sec (should be rounded down to 1 min) later null, false )!! assertEquals(Related.START, ref) - assertEquals(-(60 * 24 + 60 + 1 + 1) *//* duration of event: *//* - 1, min) + assertEquals(-(60 * 24 + 60 + 1 + 1) /* duration of event: */ - 1, min) } @Test fun testVAlarm_TriggerPeriod() { val (ref, min) = ICalendar.vAlarmToMin( VAlarm(Period.parse("-P1W1D")), - DtStart(net.fortuna.ical4j.model.Date(currentTime)), null, null, + DtStart(currentTime), null, null, false )!! assertEquals(Related.START, ref) @@ -340,12 +339,12 @@ class ICalendarTest { @Test fun testVAlarm_TriggerAbsoluteValue() { // TRIGGER;VALUE=DATE-TIME: - val alarm = VAlarm(DateTime(currentTime - 89 * 1000)) // 89 sec (should be cut off to 1 min) before event - alarm.trigger.parameters.add(Related.END) // not useful for DATE-TIME values, should be ignored - val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(DateTime(currentTime)), null, null, false)!! + val alarm = VAlarm(currentTime.minusSeconds(89).toInstant()) // 89 sec (should be cut off to 1 min) before event + alarm.getProperty(TRIGGER).getOrNull()?.add(Related.END) // not useful for DATE-TIME values, should be ignored + val (ref, min) = ICalendar.vAlarmToMin(alarm, DtStart(currentTime), null, null, false)!! assertEquals(Related.START, ref) assertEquals(1, min) - }*/ + } // TODO Note: can we use the following now when we have ical4j 4.x? From b63f04a418e9f7560df8c6bfe8c98e7badcac3ed Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 10 Mar 2026 17:48:33 +0100 Subject: [PATCH 07/24] [ical4j 4.x] Re-enable tests that don't require changes (#220) * Enable `DescriptionBuilderTest` * Enable `LocationBuilderTest` * Enable `SequenceBuilderTest` * Enable `TitleBuilderTest` * Enable `UrlBuilderTest` --- .../mapping/calendar/builder/DescriptionBuilderTest.kt | 2 -- .../synctools/mapping/calendar/builder/LocationBuilderTest.kt | 2 -- .../synctools/mapping/calendar/builder/SequenceBuilderTest.kt | 2 -- .../synctools/mapping/calendar/builder/TitleBuilderTest.kt | 2 -- .../synctools/mapping/calendar/builder/UrlBuilderTest.kt | 2 -- 5 files changed, 10 deletions(-) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt index 8935b9ec..dc908764 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/DescriptionBuilderTest.kt @@ -15,12 +15,10 @@ import net.fortuna.ical4j.model.property.Description import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class DescriptionBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt index a2fde917..e6149e95 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/LocationBuilderTest.kt @@ -15,12 +15,10 @@ import net.fortuna.ical4j.model.property.Location import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class LocationBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt index aa09cb10..b6313ce1 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/SequenceBuilderTest.kt @@ -14,12 +14,10 @@ import at.bitfire.synctools.storage.calendar.EventsContract import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Sequence -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class SequenceBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt index de976ea9..e4af9ca3 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/TitleBuilderTest.kt @@ -15,12 +15,10 @@ import net.fortuna.ical4j.model.property.Summary import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class TitleBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt index 3ce6a560..849f7e4f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UrlBuilderTest.kt @@ -17,13 +17,11 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Url import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.net.URI -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UrlBuilderTest { From f8b5370e795c684aefeff047a29e1eadc4729d14 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 12 Mar 2026 12:21:19 +0100 Subject: [PATCH 08/24] [ical4j 4.x] Migrate event builders part 1 (#223) * Migrate `AccessLevelBuilder` * Migrate `AllDayBuilder` * Migrate `AttendeesBuilder` * Migrate `AvailabilityBuilder` * Migrate `CategoriesBuilder` * Migrate `ColorBuilder` * Migrate `OrganizerBuilder` * Migrate `RemindersBuilder` * Migrate `StatusBuilder` * Migrate `UidBuilder` * Migrate `UnknownPropertiesBuilder` --- lib/build.gradle.kts | 10 +- .../bitfire/ical4android/UnknownProperty.kt | 8 +- .../at/bitfire/ical4android/util/DateUtils.kt | 4 +- .../mapping/calendar/AttendeeMappings.kt | 11 +- .../calendar/builder/AccessLevelBuilder.kt | 12 +- .../mapping/calendar/builder/AllDayBuilder.kt | 7 +- .../calendar/builder/AttendeesBuilder.kt | 15 +- .../calendar/builder/AvailabilityBuilder.kt | 8 +- .../calendar/builder/CategoriesBuilder.kt | 8 +- .../mapping/calendar/builder/ColorBuilder.kt | 6 +- .../calendar/builder/OrganizerBuilder.kt | 8 +- .../calendar/builder/RemindersBuilder.kt | 20 +- .../mapping/calendar/builder/StatusBuilder.kt | 9 +- .../mapping/calendar/builder/UidBuilder.kt | 4 +- .../builder/UnknownPropertiesBuilder.kt | 5 +- .../test/kotlin/at/bitfire/Ical4jHelper.kt | 31 ++ .../mapping/calendar/AttendeeMappingsTest.kt | 296 ++++++++---------- .../builder/AccessLevelBuilderTest.kt | 24 +- .../calendar/builder/AllDayBuilderTest.kt | 20 +- .../calendar/builder/AttendeesBuilderTest.kt | 135 ++++---- .../builder/AvailabilityBuilderTest.kt | 18 +- .../calendar/builder/CategoriesBuilderTest.kt | 6 +- .../calendar/builder/ColorBuilderTest.kt | 2 - .../calendar/builder/OrganizerBuilderTest.kt | 14 +- .../calendar/builder/RemindersBuilderTest.kt | 77 ++--- .../calendar/builder/StatusBuilderTest.kt | 20 +- .../calendar/builder/UidBuilderTest.kt | 2 - .../builder/UnknownPropertiesBuilderTest.kt | 10 +- 28 files changed, 381 insertions(+), 409 deletions(-) create mode 100644 lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 3d8485d1..cd66d8ee 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -137,4 +137,12 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.roboelectric) -} \ No newline at end of file +} + +tasks.withType().configureEach { + options { + // Prevent Robolectric from instrumenting ical4j classes to avoid problems with registering + // ical4j's ZoneRulesProviderImpl more than once with Java's ZoneRulesProvider. + systemProperty("org.robolectric.packagesToNotAcquire", "net.fortuna.ical4j") + } +} diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt b/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt index 66bcaa6e..45415cf8 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt @@ -16,6 +16,7 @@ import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyBuilder import net.fortuna.ical4j.model.PropertyFactory import org.json.JSONArray +import org.json.JSONObject /** * Helpers to (de)serialize unknown properties as JSON to store it in an Android ExtendedProperty row. @@ -81,13 +82,12 @@ object UnknownProperty { json.put(prop.name) json.put(prop.value) - TODO("ical4j 4.x") - /*if (!prop.parameters.isEmpty) { + if (prop.parameterList.all.isNotEmpty()) { val jsonParams = JSONObject() - for (param in prop.parameters) + for (param in prop.parameterList.all) jsonParams.put(param.name, param.value) json.put(jsonParams) - }*/ + } return json.toString() } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index 34ad0449..e4a28a35 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -7,6 +7,7 @@ package at.bitfire.ical4android.util import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.DateProperty @@ -89,8 +90,7 @@ object DateUtils { * @return *true* if the date is a DATE value; *false* otherwise (for instance, when the argument is a DATE-TIME value or null) */ fun isDate(date: DateProperty<*>?): Boolean = - TODO("ical4j 4.x") - //date != null && date.date is Date && date.date !is DateTime + date != null && !TemporalAdapter.isDateTimePrecision(date.date) /** * Determines whether a given date represents a DATE-TIME value. diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt index a9dbeeee..b55b7334 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappings.kt @@ -14,6 +14,8 @@ import net.fortuna.ical4j.model.parameter.CuType import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.Role import net.fortuna.ical4j.model.property.Attendee +import kotlin.jvm.optionals.getOrDefault +import kotlin.jvm.optionals.getOrNull /** * Defines mappings between Android [Attendees] and iCalendar parameters. @@ -113,10 +115,8 @@ object AttendeeMappings { val type: Int var relationship: Int - val cuType: CuType = TODO("ical4j 4.x") - // attendee.getParameter(Parameter.CUTYPE) ?: CuType.INDIVIDUAL - val role: Role = TODO("ical4j 4.x") - // attendee.getParameter(Parameter.ROLE) ?: Role.REQ_PARTICIPANT + val cuType = attendee.getParameter(Parameter.CUTYPE).getOrDefault(CuType.INDIVIDUAL) + val role = attendee.getParameter(Parameter.ROLE).getOrDefault(Role.REQ_PARTICIPANT) when (cuType) { CuType.RESOURCE -> { @@ -163,8 +163,7 @@ object AttendeeMappings { val email = if (uri.scheme.equals("mailto", true)) uri.schemeSpecificPart else - TODO("ical4j 4.x") - //attendee.getParameter(Parameter.EMAIL)?.value + attendee.getParameter(Parameter.EMAIL).getOrNull()?.value if (email == organizer) relationship = Attendees.RELATIONSHIP_ORGANIZER diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt index 9fa344d0..72e8dbea 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilder.kt @@ -20,20 +20,19 @@ class AccessLevelBuilder: AndroidEntityBuilder { val accessLevel: Int val retainValue: Boolean - TODO("ical4j 4.x") - /*val classification = from.classification - when (classification) { - Clazz.PUBLIC -> { + val classification: Clazz? = from.classification + when (classification?.value?.uppercase()) { + Clazz.VALUE_PUBLIC -> { accessLevel = Events.ACCESS_PUBLIC retainValue = false } - Clazz.PRIVATE -> { + Clazz.VALUE_PRIVATE -> { accessLevel = Events.ACCESS_PRIVATE retainValue = false } - Clazz.CONFIDENTIAL -> { + Clazz.VALUE_CONFIDENTIAL -> { accessLevel = Events.ACCESS_CONFIDENTIAL retainValue = true } @@ -61,7 +60,6 @@ class AccessLevelBuilder: AndroidEntityBuilder { ExtendedProperties.VALUE to UnknownProperty.toJsonString(classification) ) ) - */ } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt index e56d2821..6ca66ed2 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilder.kt @@ -9,14 +9,15 @@ package at.bitfire.synctools.mapping.calendar.builder import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.ical4android.util.DateUtils +import at.bitfire.synctools.icalendar.dtStart import net.fortuna.ical4j.model.component.VEvent +import java.time.temporal.Temporal class AllDayBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - TODO("ical4j 4.x") - /*val allDay = DateUtils.isDate(from.startDate) - to.entityValues.put(Events.ALL_DAY, if (allDay) 1 else 0)*/ + val allDay = DateUtils.isDate(from.dtStart()) + to.entityValues.put(Events.ALL_DAY, if (allDay) 1 else 0) } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt index 9c93a13d..0d770091 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilder.kt @@ -19,6 +19,7 @@ import net.fortuna.ical4j.model.parameter.Cn import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.PartStat import net.fortuna.ical4j.model.property.Attendee +import kotlin.jvm.optionals.getOrNull class AttendeesBuilder( private val calendar: AndroidCalendar @@ -36,34 +37,33 @@ class AttendeesBuilder( calendar.ownerAccount ?: calendar.account.name val member = attendee.calAddress - TODO("ical4j 4.x") - /*if (member.scheme.equals("mailto", true)) // attendee identified by email + if (member.scheme.equals("mailto", true)) // attendee identified by email values.put(Attendees.ATTENDEE_EMAIL, member.schemeSpecificPart) else { // attendee identified by other URI values.put(Attendees.ATTENDEE_ID_NAMESPACE, member.scheme) values.put(Attendees.ATTENDEE_IDENTITY, member.schemeSpecificPart) - attendee.getParameter(Parameter.EMAIL)?.let { email -> + attendee.getParameter(Parameter.EMAIL).ifPresent { email -> values.put(Attendees.ATTENDEE_EMAIL, email.value) } } - attendee.getParameter(Parameter.CN)?.let { cn -> + attendee.getParameter(Parameter.CN).ifPresent { cn -> values.put(Attendees.ATTENDEE_NAME, cn.value) } // type/relation mapping is complex and thus outsourced to AttendeeMappings AttendeeMappings.iCalendarToAndroid(attendee, values, organizer) - val status = when(attendee.getParameter(Parameter.PARTSTAT) as? PartStat) { + val status = when(attendee.getParameter(Parameter.PARTSTAT).getOrNull()) { PartStat.ACCEPTED -> Attendees.ATTENDEE_STATUS_ACCEPTED PartStat.DECLINED -> Attendees.ATTENDEE_STATUS_DECLINED PartStat.TENTATIVE -> Attendees.ATTENDEE_STATUS_TENTATIVE PartStat.DELEGATED -> Attendees.ATTENDEE_STATUS_NONE else /* default: PartStat.NEEDS_ACTION */ -> Attendees.ATTENDEE_STATUS_INVITED } - values.put(Attendees.ATTENDEE_STATUS, status)*/ + values.put(Attendees.ATTENDEE_STATUS, status) return values } @@ -75,8 +75,7 @@ class AttendeesBuilder( return if (uri.scheme.equals("mailto", true)) uri.schemeSpecificPart else - TODO("ical4j 4.x") - // organizer.getParameter(Parameter.EMAIL)?.value + organizer.getParameter(Parameter.EMAIL).getOrNull()?.value } return null } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt index 931d772f..2ad5a8f3 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilder.kt @@ -14,19 +14,19 @@ import net.fortuna.ical4j.model.property.Transp class AvailabilityBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - TODO("ical4j 4.x") - /*val availability = when (from.transparency) { - Transp.TRANSPARENT -> + val availability = when (from.timeTransparency?.value?.uppercase()) { + Transp.VALUE_TRANSPARENT -> Events.AVAILABILITY_FREE // Default value in iCalendar is OPAQUE else /* including Transp.OPAQUE */ -> Events.AVAILABILITY_BUSY } + to.entityValues.put( Events.AVAILABILITY, availability - )*/ + ) } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt index 3468ae3a..762d97f4 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilder.kt @@ -13,13 +13,13 @@ import at.bitfire.synctools.storage.calendar.EventsContract import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Categories +import kotlin.jvm.optionals.getOrNull class CategoriesBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - TODO("ical4j 4.x") - /*val categories = from.getProperty(Property.CATEGORIES)?.categories - if (categories != null && !categories.isEmpty) { + val categories = from.getProperty(Property.CATEGORIES).getOrNull()?.categories?.texts + if (!categories.isNullOrEmpty()) { val rawCategories = categories.joinToString(EventsContract.CATEGORIES_SEPARATOR.toString()) { category -> // drop occurrences of CATEGORIES_SEPARATOR in category names category.filter { it != EventsContract.CATEGORIES_SEPARATOR } @@ -32,7 +32,7 @@ class CategoriesBuilder: AndroidEntityBuilder { ExtendedProperties.VALUE to rawCategories ) ) - }*/ + } } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt index 08264452..d739382a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilder.kt @@ -14,6 +14,7 @@ import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.synctools.storage.calendar.AndroidCalendar import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Color +import kotlin.jvm.optionals.getOrNull class ColorBuilder( private val calendar: AndroidCalendar @@ -22,8 +23,7 @@ class ColorBuilder( override fun build(from: VEvent, main: VEvent, to: Entity) { val values = to.entityValues - TODO("ical4j 4.x") - /*val color = from.getProperty(Color.PROPERTY_NAME)?.value + val color = from.getProperty(Color.PROPERTY_NAME).getOrNull()?.value if (color != null && hasColor(color)) { // set event color (if it's available for this account) values.put(Events.EVENT_COLOR_KEY, color) @@ -31,7 +31,7 @@ class ColorBuilder( // reset color index and value values.putNull(Events.EVENT_COLOR_KEY) values.putNull(Events.EVENT_COLOR) - }*/ + } } @VisibleForTesting diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt index 4789c2b1..39803eac 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilder.kt @@ -17,6 +17,7 @@ import net.fortuna.ical4j.model.property.Organizer import java.net.URI import java.util.logging.Level import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull class OrganizerBuilder( private val ownerAccount: String @@ -46,17 +47,16 @@ class OrganizerBuilder( return null // Take from mailto: value or EMAIL parameter - TODO("ical4j 4.x") - /*val uri: URI? = organizer.calAddress + val uri: URI? = organizer.calAddress val email = if (uri?.scheme.equals("mailto", true)) uri?.schemeSpecificPart else - organizer.getParameter(Parameter.EMAIL)?.value + organizer.getParameter(Parameter.EMAIL).getOrNull()?.value if (email != null) return email - logger.log(Level.WARNING, "Ignoring ORGANIZER without email address (not supported by Android)", organizer)*/ + logger.log(Level.WARNING, "Ignoring ORGANIZER without email address (not supported by Android)", organizer) return null } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt index c545f4cf..bc0cd168 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilder.kt @@ -11,10 +11,15 @@ import android.content.Entity import android.provider.CalendarContract.Reminders import androidx.core.content.contentValuesOf import at.bitfire.ical4android.ICalendar +import at.bitfire.synctools.icalendar.dtStart +import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.DtEnd +import java.time.temporal.Temporal import java.util.Locale +import kotlin.jvm.optionals.getOrNull class RemindersBuilder: AndroidEntityBuilder { @@ -24,21 +29,20 @@ class RemindersBuilder: AndroidEntityBuilder { } private fun buildReminder(alarm: VAlarm, event: VEvent): ContentValues { - TODO("ical4j 4.x") - /*val method = when (alarm.action?.value?.uppercase(Locale.ROOT)) { - Action.DISPLAY.value, - Action.AUDIO.value -> Reminders.METHOD_ALERT // will trigger an alarm on the Android device + val method = when (alarm.getProperty(Property.ACTION)?.getOrNull()?.value?.uppercase()) { + Action.VALUE_DISPLAY, + Action.VALUE_AUDIO -> Reminders.METHOD_ALERT // will trigger an alarm on the Android device // Note: The calendar provider doesn't support saving specific attendees for email reminders. - Action.EMAIL.value -> Reminders.METHOD_EMAIL + Action.VALUE_EMAIL -> Reminders.METHOD_EMAIL else -> Reminders.METHOD_DEFAULT // won't trigger an alarm on the Android device } val minutes = ICalendar.vAlarmToMin( alarm = alarm, - refStart = event.startDate, - refEnd = event.endDate, + refStart = event.dtStart(), + refEnd = event.getEndDate().getOrNull(), refDuration = event.duration, allowRelEnd = false )?.second ?: Reminders.MINUTES_DEFAULT @@ -46,7 +50,7 @@ class RemindersBuilder: AndroidEntityBuilder { return contentValuesOf( Reminders.METHOD to method, Reminders.MINUTES to minutes - )*/ + ) } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt index 24a26b69..2854adeb 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilder.kt @@ -14,13 +14,12 @@ import net.fortuna.ical4j.model.property.Status class StatusBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - TODO("ical4j 4.x") - /*to.entityValues.put(Events.STATUS, when (from.status) { - Status.VEVENT_CONFIRMED -> Events.STATUS_CONFIRMED - Status.VEVENT_CANCELLED -> Events.STATUS_CANCELED + to.entityValues.put(Events.STATUS, when (from.status?.value?.uppercase()) { + Status.VALUE_CONFIRMED -> Events.STATUS_CONFIRMED + Status.VALUE_CANCELLED -> Events.STATUS_CANCELED null -> null else -> Events.STATUS_TENTATIVE - })*/ + }) } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt index bbd722bf..63129842 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilder.kt @@ -9,14 +9,14 @@ package at.bitfire.synctools.mapping.calendar.builder import android.content.Entity import android.provider.CalendarContract.Events import net.fortuna.ical4j.model.component.VEvent +import kotlin.jvm.optionals.getOrNull class UidBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { // Always take UID from main event because exceptions must have the same UID anyway. // Note: RFC 5545 requires UID for VEVENTs, however the obsoleted RFC 2445 does not. - TODO("ical4j 4.x") - /*to.entityValues.put(Events.UID_2445, main.uid?.value)*/ + to.entityValues.put(Events.UID_2445, main.uid?.getOrNull()?.value) } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt index aad9959b..dcbc983a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilder.kt @@ -48,10 +48,9 @@ class UnknownPropertiesBuilder: AndroidEntityBuilder { @VisibleForTesting internal fun unknownProperties(event: VEvent): List = - TODO("ical4j 4.x") - /*event.properties.filterNot { + event.propertyList.all.filterNot { KNOWN_PROPERTY_NAMES.contains(it.name.uppercase()) - }*/ + } companion object { diff --git a/lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt b/lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt new file mode 100644 index 00000000..6a23e6fd --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt @@ -0,0 +1,31 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire + +import net.fortuna.ical4j.model.CalendarDateFormat +import net.fortuna.ical4j.model.TemporalAdapter +import net.fortuna.ical4j.model.TimeZone +import java.time.LocalDateTime +import java.time.ZonedDateTime +import java.time.temporal.Temporal + +fun dateTimeValue(value: String): Temporal { + return if (value.endsWith("Z")) { + TemporalAdapter.parse(value, CalendarDateFormat.UTC_DATE_TIME_FORMAT).temporal + } else { + TemporalAdapter.parse(value, CalendarDateFormat.FLOATING_DATE_TIME_FORMAT).temporal + } +} + +fun dateTimeValue(value: String, timeZone: TimeZone): ZonedDateTime { + val temporal = dateTimeValue(value) + return if (temporal is LocalDateTime) { + temporal.atZone(timeZone.toZoneId()) + } else { + error("Unexpected temporal type: ${temporal::class}") + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt index 99c15246..278ae09a 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AttendeeMappingsTest.kt @@ -13,27 +13,23 @@ import net.fortuna.ical4j.model.parameter.Role import net.fortuna.ical4j.model.property.Attendee import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AttendeeMappingsTest { companion object { const val DEFAULT_ORGANIZER = "organizer@example.com" - val CuTypeFancy = net.fortuna.ical4j.model.parameter.CuType("X-FANCY") + val CuTypeFancy = CuType("X-FANCY") val RoleFancy = Role("X-FANCY") } - init { - TODO("ical4j 4.x") - } - - /*@Test + /* + @Ignore("ical4j 4.x") + @Test fun testAndroidToICalendar_TypeRequired_RelationshipAttendee() { testAndroidToICalendar(ContentValues().apply { put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED) @@ -388,6 +384,7 @@ class AttendeeMappingsTest { ) } } + */ @@ -409,9 +406,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeNone_RoleChair() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(Role.CHAIR) - }) { + .add(Role.CHAIR) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -427,9 +423,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeNone_RoleReqParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(Role.REQ_PARTICIPANT) - }) { + .add(Role.REQ_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -445,9 +440,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeNone_RoleOptParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(Role.OPT_PARTICIPANT) - }) { + .add(Role.OPT_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_OPTIONAL, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -463,9 +457,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeNone_RoleNonParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(Role.NON_PARTICIPANT) - }) { + .add(Role.NON_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_NONE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -481,9 +474,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeNone_RoleXValue() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(RoleFancy) - }) { + .add(RoleFancy) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -500,9 +492,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeIndividual_RoleNone() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(net.fortuna.ical4j.model.parameter.CuType.INDIVIDUAL) - }) { + .add(CuType.INDIVIDUAL) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -518,10 +509,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeIndividual_RoleChair() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.INDIVIDUAL) - parameters.add(Role.CHAIR) - }) { + .add(CuType.INDIVIDUAL) + .add(Role.CHAIR) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -537,10 +527,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeIndividual_RoleReqParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.INDIVIDUAL) - parameters.add(Role.REQ_PARTICIPANT) - }) { + .add(CuType.INDIVIDUAL) + .add(Role.REQ_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -556,10 +545,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeIndividual_RoleOptParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.INDIVIDUAL) - parameters.add(Role.OPT_PARTICIPANT) - }) { + .add(CuType.INDIVIDUAL) + .add(Role.OPT_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_OPTIONAL, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -575,10 +563,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeIndividual_RoleNonParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.INDIVIDUAL) - parameters.add(Role.NON_PARTICIPANT) - }) { + .add(CuType.INDIVIDUAL) + .add(Role.NON_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_NONE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -594,10 +581,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeIndividual_RoleXValue() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.INDIVIDUAL) - parameters.add(RoleFancy) - }) { + .add(CuType.INDIVIDUAL) + .add(RoleFancy) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -614,9 +600,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeUnknown_RoleNone() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.UNKNOWN) - }) { + .add(CuType.UNKNOWN) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -632,10 +617,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeUnknown_RoleChair() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.UNKNOWN) - parameters.add(Role.CHAIR) - }) { + .add(CuType.UNKNOWN) + .add(Role.CHAIR) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -651,10 +635,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeUnknown_RoleReqParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.UNKNOWN) - parameters.add(Role.REQ_PARTICIPANT) - }) { + .add(CuType.UNKNOWN) + .add(Role.REQ_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -670,10 +653,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeUnknown_RoleOptParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.UNKNOWN) - parameters.add(Role.OPT_PARTICIPANT) - }) { + .add(CuType.UNKNOWN) + .add(Role.OPT_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_OPTIONAL, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -689,10 +671,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeUnknown_RoleNonParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.UNKNOWN) - parameters.add(Role.NON_PARTICIPANT) - }) { + .add(CuType.UNKNOWN) + .add(Role.NON_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_NONE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -708,10 +689,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeUnknown_RoleXValue() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.UNKNOWN) - parameters.add(RoleFancy) - }) { + .add(CuType.UNKNOWN) + .add(RoleFancy) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -728,9 +708,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeGroup_RoleNone() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.GROUP) - }) { + .add(CuType.GROUP) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -746,10 +725,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeGroup_RoleChair() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.GROUP) - parameters.add(Role.CHAIR) - }) { + .add(CuType.GROUP) + .add(Role.CHAIR) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -765,10 +743,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeGroup_RoleReqParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.GROUP) - parameters.add(Role.REQ_PARTICIPANT) - }) { + .add(CuType.GROUP) + .add(Role.REQ_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -784,10 +761,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeGroup_RoleOptParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.GROUP) - parameters.add(Role.OPT_PARTICIPANT) - }) { + .add(CuType.GROUP) + .add(Role.OPT_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_OPTIONAL, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -803,10 +779,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeGroup_RoleNonParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.GROUP) - parameters.add(Role.NON_PARTICIPANT) - }) { + .add(CuType.GROUP) + .add(Role.NON_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_NONE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -822,10 +797,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeGroup_RoleXValue() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.GROUP) - parameters.add(RoleFancy) - }) { + .add(CuType.GROUP) + .add(RoleFancy) + ) { assertEquals( Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -842,9 +816,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeResource_RoleNone() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.RESOURCE) - }) { + .add(CuType.RESOURCE) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -860,10 +833,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeResource_RoleChair() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.RESOURCE) - parameters.add(Role.CHAIR) - }) { + .add(CuType.RESOURCE) + .add(Role.CHAIR) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -879,10 +851,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeResource_RoleReqParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.RESOURCE) - parameters.add(Role.REQ_PARTICIPANT) - }) { + .add(CuType.RESOURCE) + .add(Role.REQ_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -898,10 +869,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeResource_RoleOptParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.RESOURCE) - parameters.add(Role.OPT_PARTICIPANT) - }) { + .add(CuType.RESOURCE) + .add(Role.OPT_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -917,10 +887,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeResource_RoleNonParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.RESOURCE) - parameters.add(Role.NON_PARTICIPANT) - }) { + .add(CuType.RESOURCE) + .add(Role.NON_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -936,10 +905,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeResource_RoleXValue() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.RESOURCE) - parameters.add(RoleFancy) - }) { + .add(CuType.RESOURCE) + .add(RoleFancy) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -956,9 +924,8 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeRoom_RoleNone() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.ROOM) - }) { + .add(CuType.ROOM) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -974,10 +941,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeRoom_RoleChair() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.ROOM) - parameters.add(Role.CHAIR) - }) { + .add(CuType.ROOM) + .add(Role.CHAIR) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -993,10 +959,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeRoom_RoleReqParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.ROOM) - parameters.add(Role.REQ_PARTICIPANT) - }) { + .add(CuType.ROOM) + .add(Role.REQ_PARTICIPANT) + ) { assertEquals( Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE) @@ -1012,10 +977,9 @@ class AttendeeMappingsTest { fun testICalendarToAndroid_CuTypeRoom_RoleOptParticipant() { testICalendarToAndroid( Attendee("mailto:attendee@example.com") - .apply { - parameters.add(CuType.ROOM) - parameters.add(Role.OPT_PARTICIPANT) - }) { + .add(CuType.ROOM) + .add(Role.OPT_PARTICIPANT) + ) { assertEquals(Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_PERFORMER, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1023,10 +987,11 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeRoom_RoleNonParticipant() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.ROOM) - parameters.add(Role.NON_PARTICIPANT) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuType.ROOM) + .add(Role.NON_PARTICIPANT) + ) { assertEquals(Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_PERFORMER, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1034,10 +999,11 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeRoom_RoleXValue() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.ROOM) - parameters.add(RoleFancy) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuType.ROOM) + .add(RoleFancy) + ) { assertEquals(Attendees.TYPE_RESOURCE, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_PERFORMER, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1046,9 +1012,10 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeXValue_RoleNone() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuTypeFancy) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuTypeFancy) + ) { assertEquals(Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_ATTENDEE, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1056,10 +1023,11 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeXValue_RoleChair() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuTypeFancy) - parameters.add(Role.CHAIR) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuTypeFancy) + .add(Role.CHAIR) + ) { assertEquals(Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_SPEAKER, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1067,10 +1035,11 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeXValue_RoleReqParticipant() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuTypeFancy) - parameters.add(Role.REQ_PARTICIPANT) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuTypeFancy) + .add(Role.REQ_PARTICIPANT) + ) { assertEquals(Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_ATTENDEE, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1078,10 +1047,11 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeXValue_RoleOptParticipant() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuTypeFancy) - parameters.add(Role.OPT_PARTICIPANT) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuTypeFancy) + .add(Role.OPT_PARTICIPANT) + ) { assertEquals(Attendees.TYPE_OPTIONAL, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_ATTENDEE, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1089,10 +1059,11 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeXValue_RoleNonParticipant() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuTypeFancy) - parameters.add(Role.NON_PARTICIPANT) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuTypeFancy) + .add(Role.NON_PARTICIPANT) + ) { assertEquals(Attendees.TYPE_NONE, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_ATTENDEE, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1100,10 +1071,11 @@ class AttendeeMappingsTest { @Test fun testICalendarToAndroid_CuTypeXValue_RoleXValue() { - testICalendarToAndroid(Attendee("mailto:attendee@example.com").apply { - parameters.add(CuTypeFancy) - parameters.add(RoleFancy) - }) { + testICalendarToAndroid( + Attendee("mailto:attendee@example.com") + .add(CuTypeFancy) + .add(RoleFancy) + ) { assertEquals(Attendees.TYPE_REQUIRED, getAsInteger(Attendees.ATTENDEE_TYPE)) assertEquals(Attendees.RELATIONSHIP_ATTENDEE, getAsInteger(Attendees.ATTENDEE_RELATIONSHIP)) } @@ -1131,6 +1103,6 @@ class AttendeeMappingsTest { val attendee = Attendee() AttendeeMappings.androidToICalendar(values, attendee) test(attendee) - }*/ + } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt index dc0fc6e7..f4d2607e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AccessLevelBuilderTest.kt @@ -16,13 +16,12 @@ import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.immutable.ImmutableClazz import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AccessLevelBuilderTest { @@ -45,12 +44,11 @@ class AccessLevelBuilderTest { @Test fun `Classification is PUBLIC`() { val result = Entity(ContentValues()) - TODO("ical4j 4.x") - /*builder.build( - from = VEvent(propertyListOf(Clazz.PUBLIC)), + builder.build( + from = VEvent(propertyListOf(ImmutableClazz.PUBLIC)), main = VEvent(), to = result - )*/ + ) assertContentValuesEqual(contentValuesOf( Events.ACCESS_LEVEL to Events.ACCESS_PUBLIC ), result.entityValues) @@ -60,12 +58,11 @@ class AccessLevelBuilderTest { @Test fun `Classification is PRIVATE`() { val result = Entity(ContentValues()) - TODO("ical4j 4.x") - /*builder.build( - from = VEvent(propertyListOf(Clazz.PRIVATE)), + builder.build( + from = VEvent(propertyListOf(ImmutableClazz.PRIVATE)), main = VEvent(), to = result - )*/ + ) assertContentValuesEqual(contentValuesOf( Events.ACCESS_LEVEL to Events.ACCESS_PRIVATE ), result.entityValues) @@ -75,12 +72,11 @@ class AccessLevelBuilderTest { @Test fun `Classification is CONFIDENTIAL`() { val result = Entity(ContentValues()) - TODO("ical4j 4.x") - /*builder.build( - from = VEvent(propertyListOf(Clazz.CONFIDENTIAL)), + builder.build( + from = VEvent(propertyListOf(ImmutableClazz.CONFIDENTIAL)), main = VEvent(), to = result - )*/ + ) assertContentValuesEqual(contentValuesOf( Events.ACCESS_LEVEL to Events.ACCESS_CONFIDENTIAL diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt index e0c80d54..f9e8cd51 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AllDayBuilderTest.kt @@ -12,16 +12,14 @@ import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.test.assertContentValuesEqual -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.time.LocalDate +import java.time.LocalDateTime -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AllDayBuilderTest { @@ -43,12 +41,11 @@ class AllDayBuilderTest { @Test fun `DTSTART is DATE`() { val result = Entity(ContentValues()) - TODO("ical4j 4.x") - /*builder.build( - from = VEvent(propertyListOf(DtStart(Date()))), + builder.build( + from = VEvent(propertyListOf(DtStart(LocalDate.now()))), main = VEvent(), to = result - )*/ + ) assertContentValuesEqual(contentValuesOf( Events.ALL_DAY to 1 ), result.entityValues) @@ -57,12 +54,11 @@ class AllDayBuilderTest { @Test fun `DTSTART is DATE-TIME`() { val result = Entity(ContentValues()) - TODO("ical4j 4.x") - /*builder.build( - from = VEvent(propertyListOf(DtStart(DateTime()))), + builder.build( + from = VEvent(propertyListOf(DtStart(LocalDateTime.now()))), main = VEvent(), to = result - )*/ + ) assertContentValuesEqual(contentValuesOf( Events.ALL_DAY to 0 ), result.entityValues) diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt index 5a0dd554..4a15ae2a 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AttendeesBuilderTest.kt @@ -10,6 +10,7 @@ import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract.Attendees import androidx.core.content.contentValuesOf +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.synctools.storage.calendar.AndroidCalendar import at.bitfire.synctools.test.assertContentValuesEqual import io.mockk.every @@ -24,13 +25,11 @@ import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.Organizer import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.net.URI -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AttendeesBuilderTest { @@ -41,16 +40,12 @@ class AttendeesBuilderTest { private val builder = AttendeesBuilder(mockCalendar) - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun `Attendee is email address`() { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee1@example.com") + this += Attendee("mailto:attendee1@example.com") }, main = VEvent(), to = result @@ -63,7 +58,7 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("https://example.com/principals/attendee") + this += Attendee("https://example.com/principals/attendee") }, main = VEvent(), to = result @@ -79,8 +74,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("sample:uri").apply { - parameters.add(Email("attendee1@example.com")) + this += Attendee("sample:uri").apply { + add(Email("attendee1@example.com")) } }, main = VEvent(), @@ -98,8 +93,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(Cn("Sample Attendee")) + this += Attendee("mailto:attendee@example.com").apply { + add(Cn("Sample Attendee")) } }, main = VEvent(), @@ -116,11 +111,11 @@ class AttendeesBuilderTest { val reqParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { + this += Attendee("mailto:attendee@example.com").apply { if (cuType != null) - parameters.add(cuType) + add(cuType) if (role != null) - parameters.add(role) + add(role) } }, main = VEvent(), @@ -136,10 +131,10 @@ class AttendeesBuilderTest { val optParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { + this += Attendee("mailto:attendee@example.com").apply { if (cuType != null) - parameters.add(cuType) - parameters.add(Role.OPT_PARTICIPANT) + add(cuType) + add(Role.OPT_PARTICIPANT) } }, main = VEvent(), @@ -154,10 +149,10 @@ class AttendeesBuilderTest { val nonParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { + this += Attendee("mailto:attendee@example.com").apply { if (cuType != null) - parameters.add(cuType) - parameters.add(Role.NON_PARTICIPANT) + add(cuType) + add(Role.NON_PARTICIPANT) } }, main = VEvent(), @@ -177,10 +172,10 @@ class AttendeesBuilderTest { val reqParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.UNKNOWN) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.UNKNOWN) if (role != null) - parameters.add(role) + add(role) } }, main = VEvent(), @@ -197,9 +192,9 @@ class AttendeesBuilderTest { val optParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.UNKNOWN) - parameters.add(Role.OPT_PARTICIPANT) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.UNKNOWN) + add(Role.OPT_PARTICIPANT) } }, main = VEvent(), @@ -215,9 +210,9 @@ class AttendeesBuilderTest { val nonParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.UNKNOWN) - parameters.add(Role.NON_PARTICIPANT) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.UNKNOWN) + add(Role.NON_PARTICIPANT) } }, main = VEvent(), @@ -237,10 +232,10 @@ class AttendeesBuilderTest { val reqParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.GROUP) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.GROUP) if (role != null) - parameters.add(role) + add(role) } }, main = VEvent(), @@ -257,9 +252,9 @@ class AttendeesBuilderTest { val optParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.GROUP) - parameters.add(Role.OPT_PARTICIPANT) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.GROUP) + add(Role.OPT_PARTICIPANT) } }, main = VEvent(), @@ -275,9 +270,9 @@ class AttendeesBuilderTest { val nonParticipant = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.GROUP) - parameters.add(Role.NON_PARTICIPANT) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.GROUP) + add(Role.NON_PARTICIPANT) } }, main = VEvent(), @@ -296,10 +291,10 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.RESOURCE) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.RESOURCE) if (role != null) - parameters.add(role) + add(role) } }, main = VEvent(), @@ -316,9 +311,9 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.RESOURCE) - parameters.add(Role.CHAIR) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.RESOURCE) + add(Role.CHAIR) } }, main = VEvent(), @@ -337,10 +332,10 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(CuType.ROOM) + this += Attendee("mailto:attendee@example.com").apply { + add(CuType.ROOM) if (role != null) - parameters.add(role) + add(role) } }, main = VEvent(), @@ -360,10 +355,10 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { + this += Attendee("mailto:attendee@example.com").apply { if (cuType != null) - parameters.add(cuType) - parameters.add(Role.CHAIR) + add(cuType) + add(Role.CHAIR) } }, main = VEvent(), @@ -382,7 +377,7 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee(URI("mailto", accountName, null)) + this += Attendee(URI("mailto", accountName, null)) }, main = VEvent(), to = result @@ -400,7 +395,7 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com") + this += Attendee("mailto:attendee@example.com") }, main = VEvent(), to = result @@ -413,8 +408,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(PartStat.NEEDS_ACTION) + this += Attendee("mailto:attendee@example.com").apply { + add(PartStat.NEEDS_ACTION) } }, main = VEvent(), @@ -428,8 +423,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(PartStat.ACCEPTED) + this += Attendee("mailto:attendee@example.com").apply { + add(PartStat.ACCEPTED) } }, main = VEvent(), @@ -443,8 +438,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(PartStat.DECLINED) + this += Attendee("mailto:attendee@example.com").apply { + add(PartStat.DECLINED) } }, main = VEvent(), @@ -458,8 +453,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(PartStat.TENTATIVE) + this += Attendee("mailto:attendee@example.com").apply { + add(PartStat.TENTATIVE) } }, main = VEvent(), @@ -473,8 +468,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(PartStat.DELEGATED) + this += Attendee("mailto:attendee@example.com").apply { + add(PartStat.DELEGATED) } }, main = VEvent(), @@ -488,8 +483,8 @@ class AttendeesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - properties += Attendee("mailto:attendee@example.com").apply { - parameters.add(PartStat("X-WILL-ASK")) + this += Attendee("mailto:attendee@example.com").apply { + add(PartStat("X-WILL-ASK")) } }, main = VEvent(), @@ -507,8 +502,8 @@ class AttendeesBuilderTest { @Test fun testOrganizerEmail_EmailParameter() { assertEquals("organizer@example.com", builder.organizerEmail(VEvent().apply { - properties += Organizer("SomeFancyOrganizer").apply { - parameters.add(Email("organizer@example.com")) + this += Organizer("SomeFancyOrganizer").apply { + add(Email("organizer@example.com")) } })) } @@ -516,7 +511,7 @@ class AttendeesBuilderTest { @Test fun testOrganizerEmail_MailtoValue() { assertEquals("organizer@example.com", builder.organizerEmail(VEvent().apply { - properties += Organizer("mailto:organizer@example.com") + this += Organizer("mailto:organizer@example.com") })) } @@ -530,6 +525,6 @@ class AttendeesBuilderTest { result.subValues.first { it.uri == Attendees.CONTENT_URI }.values, onlyFieldsInExpected = true ) - }*/ + } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt index 87393604..8b7b3f8c 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AvailabilityBuilderTest.kt @@ -11,15 +11,13 @@ import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.synctools.icalendar.propertyListOf import net.fortuna.ical4j.model.component.VEvent -import net.fortuna.ical4j.model.property.Transp +import net.fortuna.ical4j.model.property.immutable.ImmutableTransp import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AvailabilityBuilderTest { @@ -40,24 +38,22 @@ class AvailabilityBuilderTest { @Test fun `Transparency is OPAQUE`() { val result = Entity(ContentValues()) - TODO("ical4j 4.x") - /*builder.build( - from = VEvent(propertyListOf(Transp.OPAQUE)), + builder.build( + from = VEvent(propertyListOf(ImmutableTransp.OPAQUE)), main = VEvent(), to = result - )*/ + ) assertEquals(Events.AVAILABILITY_BUSY, result.entityValues.get(Events.AVAILABILITY)) } @Test fun `Transparency is TRANSPARENT`() { val result = Entity(ContentValues()) - TODO("ical4j 4.x") - /*builder.build( - from = VEvent(propertyListOf(Transp.TRANSPARENT)), + builder.build( + from = VEvent(propertyListOf(ImmutableTransp.TRANSPARENT)), main = VEvent(), to = result - )*/ + ) assertEquals(Events.AVAILABILITY_FREE, result.entityValues.get(Events.AVAILABILITY)) } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt index 06ca80be..9de5584f 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/CategoriesBuilderTest.kt @@ -10,6 +10,7 @@ import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract.ExtendedProperties import androidx.core.content.contentValuesOf +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.synctools.storage.calendar.EventsContract import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.TextList @@ -17,12 +18,10 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Categories import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class CategoriesBuilderTest { @@ -33,8 +32,7 @@ class CategoriesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - TODO("ical4j 4.x") - // properties += Categories(TextList(arrayOf("Cat 1", "Cat\\2"))) + this += Categories(TextList("Cat 1", "Cat\\2")) }, main = VEvent(), to = result diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt index 9cbe72ea..1470f2f2 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/ColorBuilderTest.kt @@ -19,13 +19,11 @@ import io.mockk.junit4.MockKRule import io.mockk.mockk import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Color -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class ColorBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt index 94ce3a12..89cbef52 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/OrganizerBuilderTest.kt @@ -16,12 +16,10 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.Organizer -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class OrganizerBuilderTest { @@ -48,8 +46,7 @@ class OrganizerBuilderTest { builder.build( from = VEvent(propertyListOf(Organizer("mailto:organizer@example.com"))).apply { // at least one attendee to make event group-scheduled - TODO("ical4j 4.x") - // properties += Attendee("mailto:attendee@example.com") + add(Attendee("mailto:attendee@example.com")) }, main = VEvent(), to = result @@ -66,8 +63,7 @@ class OrganizerBuilderTest { builder.build( from = VEvent(propertyListOf(Organizer("local-id:user"))).apply { // at least one attendee to make event group-scheduled - TODO("ical4j 4.x") - // properties += Attendee("mailto:attendee@example.com") + add(Attendee("mailto:attendee@example.com")) }, main = VEvent(), to = result @@ -83,10 +79,8 @@ class OrganizerBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent(propertyListOf( - Organizer("local-id:user").apply { - TODO("ical4j 4.x") - // parameters.add(Email("organizer@example.com")) - }, + Organizer("local-id:user") + .add(Email("organizer@example.com")), Attendee("mailto:attendee@example.com") )), main = VEvent(), diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt index 22b03f02..3a1ab7c7 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/RemindersBuilderTest.kt @@ -10,10 +10,12 @@ import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract.Reminders import androidx.core.content.contentValuesOf +import at.bitfire.dateTimeValue +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.ComponentList -import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VEvent @@ -21,14 +23,15 @@ import net.fortuna.ical4j.model.parameter.Related import net.fortuna.ical4j.model.property.Action import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Trigger +import net.fortuna.ical4j.model.property.immutable.ImmutableAction import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.time.Duration import java.time.Period -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class RemindersBuilderTest { @@ -38,16 +41,12 @@ class RemindersBuilderTest { private val builder = RemindersBuilder() - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun `No trigger`() { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm() + this += VAlarm() }, main = VEvent(), to = result @@ -63,8 +62,8 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm(java.time.Duration.ofMinutes(-10)).apply { - properties += Action.AUDIO + this += VAlarm(Duration.ofMinutes(-10)).apply { + this += ImmutableAction.AUDIO } }, main = VEvent(), @@ -81,8 +80,8 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm(java.time.Duration.ofMinutes(-10)).apply { - properties += Action.DISPLAY + this += VAlarm(Duration.ofMinutes(-10)).apply { + this += ImmutableAction.DISPLAY } }, main = VEvent(), @@ -99,8 +98,8 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm(java.time.Duration.ofSeconds(-120)).apply { - properties += Action.EMAIL + this += VAlarm(Duration.ofSeconds(-120)).apply { + this += ImmutableAction.EMAIL } }, main = VEvent(), @@ -117,8 +116,8 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm(java.time.Duration.ofSeconds(-120)).apply { - properties += Action("X-CUSTOM") + this += VAlarm(Duration.ofSeconds(-120)).apply { + this += Action("X-CUSTOM") } }, main = VEvent(), @@ -135,7 +134,7 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm(Period.ofDays(-1)) + this += VAlarm(Period.ofDays(-1)) }, main = VEvent(), to = result @@ -148,7 +147,7 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm(java.time.Duration.ofSeconds(-10)) + this += VAlarm(Duration.ofSeconds(-10)) }, main = VEvent(), to = result @@ -161,7 +160,7 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent().apply { - components += VAlarm(java.time.Duration.ofMinutes(10)) + this += VAlarm(Duration.ofMinutes(10)) }, main = VEvent(), to = result @@ -175,11 +174,11 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent(propertyListOf( - DtStart(DateTime("20200621T120000", tzVienna)), - DtEnd(DateTime("20200621T140000", tzVienna)) + DtStart(dateTimeValue("20200621T120000", tzVienna)), + DtEnd(dateTimeValue("20200621T140000", tzVienna)) ), ComponentList(listOf( VAlarm(Period.ofDays(-1)).apply { - trigger.parameters.add(Related.END) + triggerProperty.add(Related.END) } ))), main = VEvent(), @@ -193,11 +192,11 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent(propertyListOf( - DtStart(DateTime("20200621T120000", tzVienna)), - DtEnd(DateTime("20200621T140000", tzVienna)) + DtStart(dateTimeValue("20200621T120000", tzVienna)), + DtEnd(dateTimeValue("20200621T140000", tzVienna)) ), ComponentList(listOf( - VAlarm(java.time.Duration.ofSeconds(-7240)).apply { - trigger.parameters.add(Related.END) + VAlarm(Duration.ofSeconds(-7240)).apply { + triggerProperty.add(Related.END) } ))), main = VEvent(), @@ -211,12 +210,11 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent(propertyListOf( - DtStart(DateTime("20200621T120000", tzVienna)), - DtEnd(DateTime("20200621T140000", tzVienna)) + DtStart(dateTimeValue("20200621T120000", tzVienna)), + DtEnd(dateTimeValue("20200621T140000", tzVienna)) ), ComponentList(listOf( - VAlarm(java.time.Duration.ofMinutes(10)).apply { - trigger.parameters.add(Related.END) - } + VAlarm(Duration.ofMinutes(10)).apply { + triggerProperty.add(Related.END) } ))), main = VEvent(), to = result @@ -230,9 +228,9 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent(propertyListOf( - DtStart(DateTime("20200621T120000", tzVienna)) + DtStart(dateTimeValue("20200621T120000", tzVienna)) ), ComponentList(listOf( - VAlarm(DateTime("20200621T110000", tzVienna)) + VAlarm(dateTimeValue("20200621T110000", tzVienna).toInstant()) ))), main = VEvent(), to = result @@ -246,15 +244,15 @@ class RemindersBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent(propertyListOf( - DtStart(DateTime("20200621T120000", tzVienna)) + DtStart(dateTimeValue("20200621T120000", tzVienna)) ), ComponentList(listOf( - VAlarm(DateTime("20200621T110000", tzShanghai)) + VAlarm(dateTimeValue("20200621T110000", tzShanghai).toInstant()) ))), main = VEvent(), to = result ) assertReminder(result, Reminders.MINUTES to 420) - }*/ + } // helpers @@ -268,4 +266,7 @@ class RemindersBuilderTest { ) } -} \ No newline at end of file +} + +private val VAlarm.triggerProperty: Trigger + get() = getProperty(Property.TRIGGER).get() diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt index afa484a6..ef6eb4ba 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StatusBuilderTest.kt @@ -11,16 +11,14 @@ import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.synctools.icalendar.propertyListOf import net.fortuna.ical4j.model.component.VEvent -import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.immutable.ImmutableStatus import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class StatusBuilderTest { @@ -38,15 +36,11 @@ class StatusBuilderTest { assertNull(result.entityValues.get(Events.STATUS)) } - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun `STATUS is CONFIRMED`() { val result = Entity(ContentValues()) builder.build( - from = VEvent(propertyListOf(Status.VEVENT_CONFIRMED)), + from = VEvent(propertyListOf(ImmutableStatus.VEVENT_CONFIRMED)), main = VEvent(), to = result ) @@ -57,7 +51,7 @@ class StatusBuilderTest { fun `STATUS is CANCELLED`() { val result = Entity(ContentValues()) builder.build( - from = VEvent(propertyListOf(Status.VEVENT_CANCELLED)), + from = VEvent(propertyListOf(ImmutableStatus.VEVENT_CANCELLED)), main = VEvent(), to = result ) @@ -68,7 +62,7 @@ class StatusBuilderTest { fun `STATUS is TENTATIVE`() { val result = Entity(ContentValues()) builder.build( - from = VEvent(propertyListOf(Status.VEVENT_TENTATIVE)), + from = VEvent(propertyListOf(ImmutableStatus.VEVENT_TENTATIVE)), main = VEvent(), to = result ) @@ -79,11 +73,11 @@ class StatusBuilderTest { fun `STATUS is invalid (for VEVENT)`() { val result = Entity(ContentValues()) builder.build( - from = VEvent(propertyListOf(Status.VTODO_IN_PROCESS)), + from = VEvent(propertyListOf(ImmutableStatus.VTODO_IN_PROCESS)), main = VEvent(), to = result ) assertEquals(Events.STATUS_TENTATIVE, result.entityValues.getAsInteger(Events.STATUS)) - }*/ + } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt index 9acca81f..ac4bd73e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UidBuilderTest.kt @@ -14,12 +14,10 @@ import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.test.assertContentValuesEqual import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Uid -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UidBuilderTest { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt index 7b4570f7..2d3a7aa7 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/UnknownPropertiesBuilderTest.kt @@ -20,12 +20,10 @@ import net.fortuna.ical4j.model.property.Uid import net.fortuna.ical4j.model.property.XProperty import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UnknownPropertiesBuilderTest { @@ -47,11 +45,9 @@ class UnknownPropertiesBuilderTest { val result = Entity(ContentValues()) builder.build( from = VEvent(propertyListOf( - XProperty("X-Some-Property", "Some Value").apply { - TODO("ical4j 4.x") - /*parameters.add(XParameter("Param1", "Value1")) - parameters.add(XParameter("Param2", "Value2"))*/ - } + XProperty("X-Some-Property", "Some Value") + .add(XParameter("Param1", "Value1")) + .add(XParameter("Param2", "Value2")) )), main = VEvent(), to = result From 6a35297920a2539062020ec19f8c09f3fcb0d728 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 12 Mar 2026 15:25:50 +0100 Subject: [PATCH 09/24] "androidify": don't use custom (ical4j) timezones (#226) * Add DatePropertyTzMapper to normalize ical4j timezones to system timezones * Deprecate timezone utilities in favor of DatePropertyTzMapper * Make systemTzId visible for testing and add unit tests * Remove deprecated AndroidTimeUtils tests for ical4j 4.x compatibility * Refactor DatePropertyTzMapper to handle OffsetDateTime and improve normalization - Convert OffsetDateTime to Instant for UTC timestamps - Extract ZonedDateTime normalization logic into separate method - Add tests for Instant, LocalDate, and OffsetDateTime handling - Improve documentation and method signatures * Fix DatePropertyTzMapperTest to use system default timezone * Add DefaultTimezoneRule for consistent timezone testing and improve systemTzId matching - Introduce `DefaultTimezoneRule` to set default timezone during tests - Update `DatePropertyTzMapperTest` to use the new rule - Refine `systemTzId` matching logic to be case-sensitive - Add tests for partial timezone ID matches - Clean up unused imports in `AndroidTimeUtils` and `DatePropertyTzMapper` * Update AndroidTimeUtils deprecation with ReplaceWith annotation * Add comment for DefaultTimezoneRule * Remove misleading KDoc from DefaultTimezoneRule --- .../at/bitfire/ical4android/util/DateUtils.kt | 57 +----- .../icalendar/DatePropertyTzMapper.kt | 113 +++++++++++ .../synctools/util/AndroidTimeUtils.kt | 73 +------ .../kotlin/at/bitfire/DefaultTimezoneRule.kt | 51 +++++ .../icalendar/DatePropertyTzMapperTest.kt | 181 ++++++++++++++++++ .../synctools/util/AndroidTimeUtilsTest.kt | 72 +------ 6 files changed, 362 insertions(+), 185 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt create mode 100644 lib/src/test/kotlin/at/bitfire/DefaultTimezoneRule.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index e4a28a35..226892d2 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -8,12 +8,10 @@ package at.bitfire.ical4android.util import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.model.TemporalAdapter -import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.DateProperty import java.io.StringReader import java.time.ZoneId -import java.util.logging.Logger /** * Date/time utilities @@ -23,48 +21,11 @@ import java.util.logging.Logger */ object DateUtils { - private val logger - get() = Logger.getLogger(javaClass.name) - - // time zones - /** - * Find the best matching Android (= available in system and Java timezone registry) - * time zone ID for a given arbitrary time zone ID: - * - * 1. Use a case-insensitive match ("EUROPE/VIENNA" will return "Europe/Vienna", - * assuming "Europe/Vienna") is available in Android. - * 2. Find partial matches (case-sensitive) in both directions, so both "Vienna" - * and "MyClient: Europe/Vienna" will return "Europe/Vienna". This shouln't be - * case-insensitive, because that would for instance return "EST" for "Westeuropäische Sommerzeit". - * 3. If nothing can be found or [tzID] is `null`, return the system default time zone. - * - * @param tzID time zone ID to be converted into Android time zone ID - * - * @return best matching Android time zone ID - */ - fun findAndroidTimezoneID(tzID: String?): String { - val availableTZs = ZoneId.getAvailableZoneIds() - var result: String? = null - - if (tzID != null) { - // first, try to find an exact match (case insensitive) - result = availableTZs.firstOrNull { it.equals(tzID, true) } - - // if that doesn't work, try to find something else that matches - if (result == null) - for (availableTZ in availableTZs) - if (availableTZ.contains(tzID) || tzID.contains(availableTZ)) { - result = availableTZ - logger.warning("Couldn't find system time zone \"$tzID\", assuming $result") - break - } - } - - // if that doesn't work, use device default as fallback - return result ?: TimeZone.getDefault().id - } + @Deprecated("Use DatePropertyTzMapper instead") + fun findAndroidTimezoneID(tzID: String?): String = + TODO("Will be removed during ical4j 4.x update") /** * Gets a [ZoneId] from a given ID string. In opposite to [ZoneId.of], @@ -74,15 +35,9 @@ object DateUtils { * * @return ZoneId or null if the argument was null or no zone with this ID could be found */ + @Deprecated("Not needed with correct mapping") fun getZoneId(id: String?): ZoneId? = - id?.let { - try { - val zone = ZoneId.of(id) - zone - } catch (_: Exception) { - null - } - } + TODO("Will be removed during ical4j 4.x update") /** * Determines whether a given date represents a DATE value. @@ -108,7 +63,7 @@ object DateUtils { * * @return parsed [VTimeZone], or `null` when the timezone definition can't be parsed */ - fun parseVTimeZone(timezoneDef: String ): VTimeZone? { + fun parseVTimeZone(timezoneDef: String): VTimeZone? { val builder = CalendarBuilder() try { val cal = builder.build(StringReader(timezoneDef)) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt new file mode 100644 index 00000000..b5d858dd --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt @@ -0,0 +1,113 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.icalendar + +import androidx.annotation.VisibleForTesting +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.parameter.TzId +import net.fortuna.ical4j.model.property.DateProperty +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.Temporal +import kotlin.jvm.optionals.getOrNull + +object DatePropertyTzMapper { + + /** + * Normalizes the date property to a system-compatible temporal representation. + * + * Processes the underlying date or date-time of the DateProperty to ensure compatibility + * with content providers by converting ical4j-specific temporal types to system-known types: + * + * - Converts OffsetDateTime to Instant (UTC timestamp). + * - Converts ZonedDateTime with ical4j-based timezones to ZonedDateTime with system-known ZoneId. + * - Leaves Instant, LocalDate, and other temporal types unchanged. + * + * @return A normalized Temporal object: + * - Instant for UTC date-times (originally OffsetDateTime). + * - ZonedDateTime with system-known ZoneId for date-times with TZID. + * - Original Temporal type for other cases (Instant, LocalDate, etc.). + */ + fun DateProperty<*>.normalizedDate(): Temporal { + /* This date is generated by ical4j's TemporalAdapter and uses + - OffsetDateTime for UTC date-times ("...Z") and + - ZonedDateTime with ical4j-based timezones for date-times with TZID. */ + val origDate: Temporal = date + + return when (origDate) { + // In content providers, there's no concept of offset date-times. We just want the UTC timestamp instead. + is OffsetDateTime -> + origDate.toInstant() + + // In case of date-time with TZID, make sure that we have a ZonedDateTime with a system-known ZoneId. + is ZonedDateTime -> + normalizeZonedDateTime( + origDate = origDate, + tzId = getParameter(Parameter.TZID).getOrNull()?.value + ) + + else -> + // return Instant and LocalDate/... as it is + origDate + } + } + + private fun normalizeZonedDateTime(origDate: ZonedDateTime, tzId: String?): ZonedDateTime { + /* In case of ZonedDateTime, date.zone.id will look like "ical4j~" (if taken from + ical4j database) or "ical4j-local-xxx" (if generated from VTIMEZONE). We want to + replace such ical4j-based timezones by system timezones because the content providers + need a system timezone ID. */ + + // Get corresponding system TZID (usually the same – "Europe/Vienna" in our example) + val systemTzId = systemTzId(tzId) + if (systemTzId != null) { + // Timezone is known by system, we want to use the same time string + // ("ddmmyyyyTHHMMSS"), but with the system timezone instead of the ical4j timezone. + val systemTz = ZoneId.of(tzId) + return origDate.withZoneSameLocal(systemTz) + } else { + // Timezone is not known by system, fall back to same timestamp, but + // with system default timezone. + return origDate.withZoneSameInstant(ZoneId.systemDefault()) + } + } + + + /** + * Attempts to find a matching system timezone ID for the given original timezone ID. + * + * This method first checks for an exact case-insensitive match among the available system timezone IDs. + * If no exact match is found, it then searches for a partial match where either the system ID contains + * the original ID or vice versa (case-insensitive comparison). + * + * @param origTzId The original timezone ID to match against system timezone IDs. Can be null. + * @return The matching system timezone ID if found, otherwise null. Returns null if the input is null. + */ + @VisibleForTesting + internal fun systemTzId(origTzId: String?): String? { + if (origTzId == null) + return null + + val systemIds = ZoneId.getAvailableZoneIds() + // First, try to find an exact match (case-insensitive) + for (systemId in systemIds) + if (origTzId.equals(systemId, ignoreCase = true)) + return systemId + + // Otherwise, try to find a partial match (sometimes the origTzId is something like + // "/freeassociation.sourceforge.net/Tzfile/Europe/Vienna"). This shouldn't be + // case-insensitive, because that would for instance return "EST" for "Westeuropäische Sommerzeit". + for (systemId in systemIds) + if (systemId.contains(origTzId) || origTzId.contains(systemId)) + return systemId + + // No result + return null + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt index 49818680..56829fae 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt @@ -6,33 +6,20 @@ package at.bitfire.synctools.util -import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.TimeApiExtensions -import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate -import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime -import at.bitfire.synctools.util.AndroidTimeUtils.androidifyTimeZone -import at.bitfire.synctools.util.AndroidTimeUtils.storageTzId import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TemporalAmountAdapter import net.fortuna.ical4j.model.TimeZoneRegistry -import net.fortuna.ical4j.model.TimeZoneRegistryFactory -import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.DateListProperty import net.fortuna.ical4j.model.property.DateProperty -import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.util.TimeZones import java.text.SimpleDateFormat import java.time.Duration import java.time.Period -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAmount import java.util.LinkedList import java.util.Locale -import java.util.TimeZone import java.util.logging.Logger object AndroidTimeUtils { @@ -54,24 +41,10 @@ object AndroidTimeUtils { get() = Logger.getLogger(javaClass.name) - /** - * Ensures that a given [net.fortuna.ical4j.model.property.DateProperty] either - * - * 1. has a time zone with an ID that is available in Android, or - * 2. is an UTC property ([net.fortuna.ical4j.model.property.DateProperty.isUtc] = *true*). - * - * To get the time zone ID which shall be given to the Calendar provider, - * use [storageTzId]. - * - * @param date [net.fortuna.ical4j.model.property.DateProperty] to validate. Values which are not DATE-TIME will be ignored. - * @param tzRegistry time zone registry to get time zones from - */ + @Deprecated("Use DatePropertyTzMapper instead", replaceWith = + ReplaceWith("date.normalizedDate()", "at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate")) fun androidifyTimeZone(date: DateProperty<*>?, tzRegistry: TimeZoneRegistry) { - TODO("ical4j 4.x") - /*if (DateUtils.isDateTime(date) && date?.isUtc == false) { - val tzID = DateUtils.findAndroidTimezoneID(date.timeZone?.id) - date.timeZone = tzRegistry.getTimeZone(tzID) - }*/ + TODO("Will be removed during ical4j 4.x update") } /** @@ -82,12 +55,14 @@ object AndroidTimeUtils { * * * @param dateList [net.fortuna.ical4j.model.property.DateListProperty] to validate. Values which are not DATE-TIME will be ignored. */ - fun androidifyTimeZone(dateList: DateListProperty<*>) { - val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() } + @Deprecated("Use DatePropertyTzMapper instead") + fun androidifyTimeZone(dateList: DateListProperty<*>): DateListProperty<*> = + TODO("Should be implemented in DatePropertyTzMapper, if needed") + /*val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() } // periods (RDate only) TODO("ical4j 4.x") - /*val periods = (dateList as? RDate)?.periods + val periods = (dateList as? RDate)?.periods if (periods != null && periods.isNotEmpty() && !periods.isUtc) { val tzID = DateUtils.findAndroidTimezoneID(periods.timeZone?.id) @@ -106,38 +81,10 @@ object AndroidTimeUtils { dateList.timeZone = tzRegistry.getTimeZone(tzID) } }*/ - } - /** - * 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 - * - the specified time zone ID for date-times with given time zone - * - the currently set default time zone ID for floating date-times - */ + @Deprecated("Implementation may vary by provider and should be done in the respective mapper") fun storageTzId(date: DateProperty<*>): String = - TODO("ical4j 4.x") - /*if (DateUtils.isDateTime(date)) { - // DATE-TIME - when { - date.isUtc -> - // DATE-TIME in UTC format - TimeZones.UTC_ID - date.timeZone != null -> - // DATE-TIME with given time-zone - date.timeZone.id - else -> - // DATE-TIME in local format (floating) - TimeZone.getDefault().id - } - } else - // DATE - TZID_UTC*/ + TODO("Will be removed during ical4j 4.x update") // recurrence sets diff --git a/lib/src/test/kotlin/at/bitfire/DefaultTimezoneRule.kt b/lib/src/test/kotlin/at/bitfire/DefaultTimezoneRule.kt new file mode 100644 index 00000000..b1bf40b7 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/DefaultTimezoneRule.kt @@ -0,0 +1,51 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire + +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.time.ZoneId +import java.util.TimeZone + +/** + * A JUnit TestRule that temporarily sets the default timezone for the duration of a test. + * + * This rule is useful for testing code that depends on the default timezone, ensuring consistent + * and predictable behavior regardless of the system's default timezone. The original default + * timezone is restored after the test completes, even if the test fails. + * + * @param defaultTzId The ID of the timezone to set as the default during the test. + */ +class DefaultTimezoneRule( + defaultTzId: String +): TestRule { + + /** The [TimeZone] corresponding to the default timezone ID provided to the rule. */ + val defaultTimeZone: TimeZone = TimeZone.getTimeZone(defaultTzId) + + /** The [ZoneId] corresponding to the default timezone ID provided to the rule. */ + val defaultZoneId: ZoneId = ZoneId.of(defaultTzId) + + override fun apply( + base: Statement, + description: Description + ): Statement = object: Statement() { + + override fun evaluate() { + val originalDefaultTz = TimeZone.getDefault() + try { + TimeZone.setDefault(defaultTimeZone) + base.evaluate() + } finally { + TimeZone.setDefault(originalDefaultTz) + } + } + + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt new file mode 100644 index 00000000..a37608b1 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt @@ -0,0 +1,181 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.icalendar + +import at.bitfire.DefaultTimezoneRule +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate +import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterList +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.parameter.TzId +import net.fortuna.ical4j.model.property.DtStart +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import java.io.StringReader +import java.time.Instant +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.time.temporal.Temporal + +class DatePropertyTzMapperTest { + + /* Sets "Europe/Vienna" as default TZ for the tests. Note that tests in this class expect that + the ical4j timestamps and system timestamps are the same, which is only guaranteed if the + system timezone rules match the ical4j timezone rules. */ + @get:Rule + val tzRule = DefaultTimezoneRule("Europe/Vienna") + + + @Test + fun `normalizedDate with TZID known to system`() { + val dtStart = DtStart( + ParameterList(listOf(TzId("Europe/Vienna"))), + "20260311T224734" + ) + + // ical4j returns ZonedDatetime with timezone from ical4j database + val ical4jDate = dtStart.date as ZonedDateTime + assertTrue(ical4jDate.zone.id.startsWith("ical4j~")) + + // normalizedDate returns ZonedDatetime (at same timestamp) with system time zone + val normalizedDate = dtStart.normalizedDate() as ZonedDateTime + assertEquals(tzRule.defaultZoneId, normalizedDate.zone) + assertEquals(ical4jDate.toInstant(), normalizedDate.toInstant()) + } + + @Test + fun `normalizedDate with TZID unknown to system`() { + val cal = CalendarBuilder().build(StringReader("BEGIN:VCALENDAR\r\n" + + "VERSION:2.0\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:Etc/ABC\n" + + "BEGIN:STANDARD\n" + + "TZNAME:-03\n" + + "TZOFFSETFROM:-0300\n" + + "TZOFFSETTO:-0300\n" + + "DTSTART:19700101T000000\n" + + "END:STANDARD\n" + + "END:VTIMEZONE\n" + + "BEGIN:VEVENT\n" + + "SUMMARY:Test Timezones\n" + + "DTSTART;TZID=Etc/ABC:20250828T130000\n" + + "END:VEVENT\n" + + "END:VCALENDAR" + )) + val vEvent = cal.getComponent(Component.VEVENT).get() + val dtStart = vEvent.requireDtStart() + + // ical4j returns ZonedDatetime with custom timezone from VTIMEZONE + val ical4jDate = dtStart.date as ZonedDateTime + assertTrue(ical4jDate.zone.id.startsWith("ical4j-local-")) + + val timestamp = Instant.ofEpochMilli( + /* 20250828T130000Z */ 1756386000000 + /* offset -0300 */ + 3*3600000 + ) + assertEquals(timestamp, ical4jDate.toInstant()) + + // normalizedDate returns ZonedDatetime (at same timestamp) with system time zone + val normalizedDate = dtStart.normalizedDate() as ZonedDateTime + assertEquals(tzRule.defaultZoneId, normalizedDate.zone) + assertEquals(timestamp, normalizedDate.toInstant()) + + // We could NOT just generate the DTSTART from the time string and the system time zone + assertNotEquals( + timestamp, + ZonedDateTime.of(2025, 8, 28, 13, 0, 0, 0, tzRule.defaultZoneId).toInstant() + ) + } + + @Test + fun `normalizedDate with Instant remains unchanged`() { + // Test that Instant dates remain unchanged + val dtStart = DtStart(Instant.now()) + + val originalDate = dtStart.date + val normalizedDate = dtStart.normalizedDate() + + assertSame(originalDate, normalizedDate) + assertTrue(normalizedDate is Instant) + } + + @Test + fun `normalizedDate with LocalDate remains unchanged`() { + // Test that LocalDate dates remain unchanged + val dtStart = DtStart("20260311") + + val originalDate = dtStart.date + val normalizedDate = dtStart.normalizedDate() + + assertSame(originalDate, normalizedDate) + assertTrue(normalizedDate is LocalDate) + } + + @Test + fun `normalizedDate with OffsetDateTime becomes an Instant`() { + // Test that OffsetDateTime dates remain unchanged + val dtStart = DtStart("20260311T224734Z") + + val originalDate = dtStart.date // OffsetDateTime + val normalizedDate = dtStart.normalizedDate() // Instant + + // Should be the same timestamp + assertEquals((originalDate as OffsetDateTime).toInstant(), normalizedDate) + } + + + @Test + fun `systemTzId with exact match`() { + val result = DatePropertyTzMapper.systemTzId("Europe/Vienna") + assertEquals("Europe/Vienna", result) + } + + @Test + fun `systemTzId with case insensitive match`() { + val result = DatePropertyTzMapper.systemTzId("europe/vienna") + assertEquals("Europe/Vienna", result) + } + + @Test + fun `systemTzId with partial match (iCalendar TZID contains system TZID)`() { + val result = DatePropertyTzMapper.systemTzId("/freeassociation.sourceforge.net/Tzfile/Europe/Vienna") + assertEquals("Europe/Vienna", result) + } + + @Test + fun `systemTzId with partial match (system TZID contains iCalendar TZID)`() { + val result = DatePropertyTzMapper.systemTzId("Vienna") + assertEquals("Europe/Vienna", result) + } + + @Test + fun `systemTzId with no match (system TZID contains iCalendar TZID, but in lowercase)`() { + val result = DatePropertyTzMapper.systemTzId("Westeuropäische Sommerzeit") + assertEquals(null, result) + } + + @Test + fun `systemTzId with null input`() { + val result = DatePropertyTzMapper.systemTzId(null) + assertEquals(null, result) + } + + @Test + fun `systemTzId with unknown timezone`() { + val result = DatePropertyTzMapper.systemTzId("Unknown/Timezone") + assertEquals(null, result) + } + + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt index e8bb8604..4c0188ff 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt @@ -63,75 +63,14 @@ class AndroidTimeUtilsTest { val tzIdDefault = java.util.TimeZone.getDefault().id!! val tzDefault = tzRegistry.getTimeZone(tzIdDefault)!! - // androidifyTimeZone - - @Test - fun testAndroidifyTimeZone_Null() { - // must not throw an exception - AndroidTimeUtils.androidifyTimeZone(null, tzRegistry) - } - - // androidifyTimeZone - // DateProperty - init { TODO("ical4j 4.x") } - /*@Test - fun testAndroidifyTimeZone_DateProperty_Date() { - // dates (without time) should be ignored - val dtStart = DtStart(Date("20150101")) - AndroidTimeUtils.androidifyTimeZone(dtStart, tzRegistry) - assertTrue(DateUtils.isDate(dtStart)) - assertNull(dtStart.timeZone) - assertFalse(dtStart.isUtc) - } - - @Test - fun testAndroidifyTimeZone_DateProperty_KnownTimeZone() { - // date-time with known time zone should be unchanged - val dtStart = DtStart("20150101T230350", tzBerlin) - AndroidTimeUtils.androidifyTimeZone(dtStart, tzRegistry) - assertEquals(tzBerlin, dtStart.timeZone) - assertFalse(dtStart.isUtc) - } - - @Test - fun testAndroidifyTimeZone_DateProperty_UnknownTimeZone() { - // time zone that is not available on Android systems should be rewritten to system default - val dtStart = DtStart("20150101T031000", tzCustom) - // 20150101T031000 CustomTime [+0310] = 20150101T000000 UTC = 1420070400 UNIX - AndroidTimeUtils.androidifyTimeZone(dtStart, tzRegistry) - assertEquals(1420070400000L, dtStart.date.time) - assertEquals(tzIdDefault, dtStart.timeZone.id) - assertFalse(dtStart.isUtc) - } - - @Test - fun testAndroidifyTimeZone_DateProperty_FloatingTime() { - // times with floating time should be treated as system default time zone - val dtStart = DtStart("20150101T230350") - AndroidTimeUtils.androidifyTimeZone(dtStart, tzRegistry) - assertEquals(DateTime("20150101T230350", tzDefault).time, dtStart.date.time) - assertEquals(tzIdDefault, dtStart.timeZone.id) - assertFalse(dtStart.isUtc) - } - - @Test - fun testAndroidifyTimeZone_DateProperty_UTC() { - // times with UTC should be unchanged - val dtStart = DtStart("20150101T230350Z") - AndroidTimeUtils.androidifyTimeZone(dtStart, tzRegistry) - assertEquals(1420153430000L, dtStart.date.time) - assertNull(dtStart.timeZone) - assertTrue(dtStart.isUtc) - } - // androidifyTimeZone // DateListProperty - date - @Test + /*@Test fun testAndroidifyTimeZone_DateListProperty_Date() { // dates (without time) should be ignored val rDate = RDate(DateList("20150101,20150102", Value.DATE)) @@ -292,15 +231,6 @@ class AndroidTimeUtilsTest { fun testStorageTzId_FloatingTime() = assertEquals(TimeZone.getDefault().id, AndroidTimeUtils.storageTzId(DtStart(DateTime("20150101T000000")))) - @Test - fun testStorageTzId_UTC() = - assertEquals(TimeZones.UTC_ID, AndroidTimeUtils.storageTzId(DtStart(DateTime("20150101T000000Z")))) - - @Test - fun testStorageTzId_ZonedTime() { - assertEquals(tzToronto.id, AndroidTimeUtils.storageTzId(DtStart("20150101T000000", tzToronto))) - } - // androidStringToRecurrenceSets From 4c10d6af06891c93a5908f39d41c311de51bc52c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Fri, 13 Mar 2026 11:56:04 +0100 Subject: [PATCH 10/24] Add logging for timezone normalization mismatches (#227) * Add logging for timezone normalization mismatches - Log warning when normalized ZonedDateTime has different timestamp than original - Add test case for TZID known to system but with different VTIMEZONE - Improve error message for unknown timezone cases * Update DatePropertyTzMapperTest to use explicit timezone assertions - Replace generic timezone assertions with specific ZonedDateTime comparisons - Change test timezone from Europe/Vienna to Europe/Berlin for consistency - Update DTSTART TZID in test iCalendar data to match new timezone --- .../icalendar/DatePropertyTzMapper.kt | 23 +++++++-- .../icalendar/DatePropertyTzMapperTest.kt | 47 +++++++++++++++++-- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt index b5d858dd..fc037b3c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt @@ -14,10 +14,15 @@ import java.time.OffsetDateTime import java.time.ZoneId import java.time.ZonedDateTime import java.time.temporal.Temporal +import java.util.logging.Level +import java.util.logging.Logger import kotlin.jvm.optionals.getOrNull object DatePropertyTzMapper { + private val logger: Logger + get() = Logger.getLogger(javaClass.name) + /** * Normalizes the date property to a system-compatible temporal representation. * @@ -37,9 +42,7 @@ object DatePropertyTzMapper { /* This date is generated by ical4j's TemporalAdapter and uses - OffsetDateTime for UTC date-times ("...Z") and - ZonedDateTime with ical4j-based timezones for date-times with TZID. */ - val origDate: Temporal = date - - return when (origDate) { + return when (val origDate: Temporal = date) { // In content providers, there's no concept of offset date-times. We just want the UTC timestamp instead. is OffsetDateTime -> origDate.toInstant() @@ -69,10 +72,20 @@ object DatePropertyTzMapper { // Timezone is known by system, we want to use the same time string // ("ddmmyyyyTHHMMSS"), but with the system timezone instead of the ical4j timezone. val systemTz = ZoneId.of(tzId) - return origDate.withZoneSameLocal(systemTz) + val result = origDate.withZoneSameLocal(systemTz) + + val origDateInstant = origDate.toInstant() + val resultInstant = result.toInstant() + if (origDateInstant != resultInstant) + logger.log(Level.WARNING, "Different timestamps of normalized $result (${resultInstant.toEpochMilli()}) " + + "and original $origDate (${origDateInstant.toEpochMilli()}) ZonedDateTime") + + return result + } else { - // Timezone is not known by system, fall back to same timestamp, but + // Timezone ID unknown or timezone not known by system, fall back to same timestamp, but // with system default timezone. + logger.log(Level.WARNING, "ZonedDateTime ($origDate) with unknown timezone ($tzId), using calculated timestamp in system default timezone") return origDate.withZoneSameInstant(ZoneId.systemDefault()) } } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt index a37608b1..7f11e049 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapperTest.kt @@ -24,7 +24,9 @@ import org.junit.Test import java.io.StringReader import java.time.Instant import java.time.LocalDate +import java.time.LocalTime import java.time.OffsetDateTime +import java.time.ZoneId import java.time.ZonedDateTime import java.time.temporal.Temporal @@ -50,10 +52,50 @@ class DatePropertyTzMapperTest { // normalizedDate returns ZonedDatetime (at same timestamp) with system time zone val normalizedDate = dtStart.normalizedDate() as ZonedDateTime - assertEquals(tzRule.defaultZoneId, normalizedDate.zone) + assertEquals(ZonedDateTime.of( + LocalDate.of(2026, 3, 11), + LocalTime.of(22, 47, 34), + ZoneId.of("Europe/Vienna") + ), normalizedDate) assertEquals(ical4jDate.toInstant(), normalizedDate.toInstant()) } + @Test + fun `normalizedDate with TZID known to system, but different VTIMEZONE`() { + val cal = CalendarBuilder().build(StringReader("BEGIN:VCALENDAR\r\n" + + "VERSION:2.0\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:Europe/Berlin\n" + + "BEGIN:STANDARD\n" + + "TZNAME:-03\n" + + "TZOFFSETFROM:-0300\n" + + "TZOFFSETTO:-0300\n" + + "DTSTART:19700101T000000\n" + + "END:STANDARD\n" + + "END:VTIMEZONE\n" + + "BEGIN:VEVENT\n" + + "SUMMARY:Test Timezones\n" + + "DTSTART;TZID=Europe/Berlin:20250828T130000\n" + + "END:VEVENT\n" + + "END:VCALENDAR" + )) + val vEvent = cal.getComponent(Component.VEVENT).get() + val dtStart = vEvent.requireDtStart() + + // ical4j returns ZonedDatetime with custom timezone from VTIMEZONE + val ical4jDate = dtStart.date as ZonedDateTime + assertTrue(ical4jDate.zone.id.startsWith("ical4j-local-")) + + // normalizedDate returns ZonedDatetime (with other timestamp because TZ OFFSET is different) with system time zone + val normalizedDate = dtStart.normalizedDate() as ZonedDateTime + assertEquals(ZonedDateTime.of( + LocalDate.of(2025, 8, 28), + LocalTime.of(13, 0, 0), + ZoneId.of("Europe/Berlin") + ), normalizedDate) + assertNotEquals(ical4jDate.toInstant(), normalizedDate.toInstant()) + } + @Test fun `normalizedDate with TZID unknown to system`() { val cal = CalendarBuilder().build(StringReader("BEGIN:VCALENDAR\r\n" + @@ -88,8 +130,7 @@ class DatePropertyTzMapperTest { // normalizedDate returns ZonedDatetime (at same timestamp) with system time zone val normalizedDate = dtStart.normalizedDate() as ZonedDateTime - assertEquals(tzRule.defaultZoneId, normalizedDate.zone) - assertEquals(timestamp, normalizedDate.toInstant()) + assertEquals(ZonedDateTime.ofInstant(timestamp, tzRule.defaultZoneId), normalizedDate) // We could NOT just generate the DTSTART from the time string and the system time zone assertNotEquals( From ba8f247db90ccbb1be496e222ee592278d0a24b1 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Mon, 16 Mar 2026 13:27:17 +0100 Subject: [PATCH 11/24] Update JtxCollection/JtxICalObject (#232) --- .../at/bitfire/ical4android/JtxCollection.kt | 16 +- .../at/bitfire/ical4android/JtxICalObject.kt | 642 ++++++++++-------- .../icalendar/DatePropertyTzMapper.kt | 46 ++ .../synctools/icalendar/Ical4jHelpers.kt | 4 + .../main/kotlin/at/techbee/jtx/JtxContract.kt | 39 +- 5 files changed, 417 insertions(+), 330 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt index a6330bb1..f3081ec1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt @@ -12,12 +12,17 @@ import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.net.Uri +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.synctools.storage.LocalStorageException import at.bitfire.synctools.storage.toContentValues import at.techbee.jtx.JtxContract import at.techbee.jtx.JtxContract.asSyncAdapter import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.component.CalendarComponent +import net.fortuna.ical4j.model.component.VJournal +import net.fortuna.ical4j.model.component.VToDo import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.immutable.ImmutableVersion import java.util.LinkedList import java.util.logging.Level import java.util.logging.Logger @@ -256,19 +261,18 @@ open class JtxCollection(val account: Account, logger.fine("getICSForCollection: found ${cursor?.count} records in ${account.name}") val ical = Calendar() - TODO("ical4j 4.x") - /*ical.properties += Version.VERSION_2_0 - ical.properties += prodId + ical += ImmutableVersion.VERSION_2_0 + ical += prodId while (cursor?.moveToNext() == true) { val jtxIcalObject = JtxICalObject(this) jtxIcalObject.populateFromContentValues(cursor.toContentValues()) val singleICS = jtxIcalObject.getICalendarFormat(prodId) - singleICS?.components?.forEach { component -> + singleICS?.getComponents()?.forEach { component -> if(component is VToDo || component is VJournal) - ical.components += component + ical.getComponents() += component } - }*/ + } return ical.toString() } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt index fcddc3cb..cce13445 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObject.kt @@ -6,27 +6,110 @@ package at.bitfire.ical4android +import android.content.ContentUris import android.content.ContentValues import android.net.Uri import android.os.ParcelFileDescriptor import android.util.Base64 import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.ICalendar.Companion.withUserAgents import at.bitfire.synctools.exception.InvalidICalendarException +import at.bitfire.synctools.icalendar.Css3Color +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDates +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.synctools.storage.BatchOperation import at.bitfire.synctools.storage.JtxBatchOperation import at.bitfire.synctools.storage.toContentValues import at.techbee.jtx.JtxContract +import at.techbee.jtx.JtxContract.JtxICalObject.TZ_ALLDAY import at.techbee.jtx.JtxContract.asSyncAdapter import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.ComponentList +import net.fortuna.ical4j.model.DateList +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterList +import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyList +import net.fortuna.ical4j.model.TextList +import net.fortuna.ical4j.model.component.CalendarComponent +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.component.VJournal +import net.fortuna.ical4j.model.component.VToDo +import net.fortuna.ical4j.model.parameter.AltRep +import net.fortuna.ical4j.model.parameter.Cn +import net.fortuna.ical4j.model.parameter.CuType +import net.fortuna.ical4j.model.parameter.DelegatedFrom +import net.fortuna.ical4j.model.parameter.DelegatedTo +import net.fortuna.ical4j.model.parameter.Dir +import net.fortuna.ical4j.model.parameter.FmtType +import net.fortuna.ical4j.model.parameter.Language +import net.fortuna.ical4j.model.parameter.Member +import net.fortuna.ical4j.model.parameter.PartStat +import net.fortuna.ical4j.model.parameter.RelType +import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.parameter.Role +import net.fortuna.ical4j.model.parameter.Rsvp +import net.fortuna.ical4j.model.parameter.SentBy +import net.fortuna.ical4j.model.parameter.TzId +import net.fortuna.ical4j.model.parameter.XParameter +import net.fortuna.ical4j.model.property.Action +import net.fortuna.ical4j.model.property.Attach +import net.fortuna.ical4j.model.property.Categories +import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.Color +import net.fortuna.ical4j.model.property.Completed +import net.fortuna.ical4j.model.property.Contact +import net.fortuna.ical4j.model.property.Created +import net.fortuna.ical4j.model.property.Description +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStamp +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.Duration +import net.fortuna.ical4j.model.property.ExDate +import net.fortuna.ical4j.model.property.Geo +import net.fortuna.ical4j.model.property.LastModified +import net.fortuna.ical4j.model.property.Location +import net.fortuna.ical4j.model.property.PercentComplete +import net.fortuna.ical4j.model.property.Priority import net.fortuna.ical4j.model.property.ProdId +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.Repeat +import net.fortuna.ical4j.model.property.Resources +import net.fortuna.ical4j.model.property.Sequence +import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.Summary +import net.fortuna.ical4j.model.property.Trigger +import net.fortuna.ical4j.model.property.Uid +import net.fortuna.ical4j.model.property.Url +import net.fortuna.ical4j.model.property.XProperty +import net.fortuna.ical4j.model.property.immutable.ImmutableAction +import net.fortuna.ical4j.model.property.immutable.ImmutablePriority +import net.fortuna.ical4j.model.property.immutable.ImmutableVersion +import java.io.FileNotFoundException import java.io.IOException import java.io.OutputStream import java.io.Reader +import java.net.URI +import java.net.URISyntaxException +import java.nio.ByteBuffer +import java.text.ParseException +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeParseException +import java.time.temporal.Temporal import java.util.UUID +import java.util.logging.Level import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull open class JtxICalObject( val collection: JtxCollection @@ -222,30 +305,28 @@ open class JtxICalObject( val iCalObjectList = mutableListOf() - TODO("ical4j 4.x") - /* - ical.components.forEach { component -> + ical.getComponents().forEach { component -> val iCalObject = JtxICalObject(collection) when(component) { is VToDo -> { iCalObject.component = JtxContract.JtxICalObject.Component.VTODO.name - if (component.uid != null) - iCalObject.uid = component.uid.value // generated UID is overwritten here (if present) - extractProperties(iCalObject, component.properties) - extractVAlarms(iCalObject, component.components) // accessing the components needs an explicit type + if (component.uid.isPresent) + iCalObject.uid = component.uid.get().value // generated UID is overwritten here (if present) + extractProperties(iCalObject, component.propertyList) + extractVAlarms(iCalObject, component.componentList) // accessing the components needs an explicit type iCalObjectList.add(iCalObject) } is VJournal -> { iCalObject.component = JtxContract.JtxICalObject.Component.VJOURNAL.name - if (component.uid != null) - iCalObject.uid = component.uid.value - extractProperties(iCalObject, component.properties) - extractVAlarms(iCalObject, component.components) // accessing the components needs an explicit type + if (component.uid.isPresent) + iCalObject.uid = component.uid.get().value + extractProperties(iCalObject, component.propertyList) + extractVAlarms(iCalObject, component.componentList) // accessing the components needs an explicit type iCalObjectList.add(iCalObject) } } - }*/ + } return iCalObjectList } @@ -256,43 +337,43 @@ open class JtxICalObject( * @param [calComponents] from which the VAlarms should be extracted */ private fun extractVAlarms(iCalObject: JtxICalObject, calComponents: ComponentList<*>) { - TODO("ical4j 4.x") - /*calComponents.forEach { component -> + calComponents.all.forEach { component -> if(component is VAlarm) { val jtxAlarm = Alarm().apply { - component.action?.value?.let { vAlarmAction -> this.action = vAlarmAction } - component.summary?.value?.let { vAlarmSummary -> this.summary = vAlarmSummary } - component.description?.value?.let { vAlarmDesc -> this.description = vAlarmDesc } - component.duration?.value?.let { vAlarmDur -> this.duration = vAlarmDur } - component.attachment?.uri?.let { uri -> this.attach = uri.toString() } - component.repeat?.value?.let { vAlarmRep -> this.repeat = vAlarmRep } + component.getProperty(Property.ACTION).getOrNull()?.let { vAlarmAction -> this.action = vAlarmAction.value } + component.summary?.let { vAlarmSummary -> this.summary = vAlarmSummary.value } + component.description?.let { vAlarmDesc -> this.description = vAlarmDesc.value } + component.getProperty(Property.DURATION).getOrNull()?.let { vAlarmDur -> this.duration = vAlarmDur.value } + component.getProperty(Property.ATTACH).getOrNull()?.uri?.let { uri -> this.attach = uri.toString() } + component.getProperty(Property.REPEAT)?.getOrNull()?.let { vAlarmRep -> this.repeat = vAlarmRep.value } // alarms can have a duration or an absolute dateTime, but not both! - if(component.trigger.duration != null) { - component.trigger?.duration?.let { duration -> this.triggerRelativeDuration = duration.toString() } - component.trigger?.getParameter(Parameter.RELATED)?.let { related -> this.triggerRelativeTo = related.value } - } else if(component.trigger.dateTime != null) { - component.trigger?.dateTime?.let { dateTime -> this.triggerTime = dateTime.time } - component.trigger?.dateTime?.timeZone?.let { timezone -> this.triggerTimezone = timezone.id } + component.getProperty(Property.TRIGGER).getOrNull()?.let { trigger -> + if(trigger.duration != null) { + trigger.duration?.let { duration -> this.triggerRelativeDuration = duration.toString() } + trigger.getParameter(Parameter.RELATED)?.getOrNull()?.let { related -> this.triggerRelativeTo = related.value } + } else if(trigger.isAbsolute) { // self-contained (not relative to dtstart/dtend) + val normalizedTrigger = trigger.normalizedDate() // Ensure timezone exists in system + this.triggerTime = normalizedTrigger.toEpochMilli() + this.triggerTimezone = normalizedTrigger.getTimeZoneId() + } } // remove properties to add the rest to other - component.properties.removeAll( - setOf( - component.action, - component.summary, - component.description, - component.duration, - component.attachment, - component.repeat, - component.trigger - ) + component.propertyList.removeAll( + Property.ACTION, + Property.SUMMARY, + Property.DESCRIPTION, + Property.DURATION, + Property.ATTACH, + Property.REPEAT, + Property.TRIGGER ) - component.properties?.let { vAlarmProps -> this.other = JtxContract.getJsonStringFromXProperties(vAlarmProps) } + component.propertyList?.let { vAlarmProps -> this.other = JtxContract.getJsonStringFromXProperties(vAlarmProps) } } iCalObject.alarms.add(jtxAlarm) } - }*/ + } } /** @@ -304,17 +385,16 @@ open class JtxICalObject( // sequence must only be null for locally created, not-yet-synchronized events iCalObject.sequence = 0 - TODO("ical4j 4.x") - /*for (prop in properties) { + for (prop in properties.all) { when (prop) { is Sequence -> iCalObject.sequence = prop.sequenceNo.toLong() - is Created -> iCalObject.created = prop.dateTime.time - is LastModified -> iCalObject.lastModified = prop.dateTime.time + is Created -> iCalObject.created = prop.date.toEpochMilli() // Instant. No need to normalize + is LastModified -> iCalObject.lastModified = prop.date.toEpochMilli() // Instant. No need to normalize is Summary -> iCalObject.summary = prop.value is Location -> { iCalObject.location = prop.value - if(!prop.parameters.isEmpty && prop.parameters.getParameter(Parameter.ALTREP) != null) - iCalObject.locationAltrep = prop.parameters.getParameter(Parameter.ALTREP).value + if(!prop.parameterList.all.isEmpty() && prop.parameterList.getFirst(Parameter.ALTREP) != null) + iCalObject.locationAltrep = prop.parameterList.getFirst(Parameter.ALTREP).getOrNull()?.value } is Geo -> { iCalObject.geoLat = prop.latitude.toDouble() @@ -327,37 +407,29 @@ open class JtxICalObject( is Priority -> iCalObject.priority = prop.level is Clazz -> iCalObject.classification = prop.value is Status -> iCalObject.status = prop.value - is DtEnd -> logger.warning("The property DtEnd must not be used for VTODO and VJOURNAL, this value is rejected.") + is DtEnd<*> -> logger.warning("The property DtEnd must not be used for VTODO and VJOURNAL, this value is rejected.") is Completed -> { - if (iCalObject.component == JtxContract.JtxICalObject.Component.VTODO.name) { - iCalObject.completed = prop.date.time - } else + if (iCalObject.component != JtxContract.JtxICalObject.Component.VTODO.name) { logger.warning("The property Completed is only supported for VTODO, this value is rejected.") + continue + } + iCalObject.completed = prop.normalizedDate().toEpochMilli() } - is Due -> { - if (iCalObject.component == JtxContract.JtxICalObject.Component.VTODO.name) { - iCalObject.due = prop.date.time - when { - prop.date is DateTime && prop.timeZone != null -> iCalObject.dueTimezone = prop.timeZone.id - prop.date is DateTime && prop.isUtc -> iCalObject.dueTimezone = TimeZone.getTimeZone("UTC").id - prop.date is DateTime && !prop.isUtc && prop.timeZone == null -> iCalObject.dueTimezone = null // this comparison is kept on purpose as "prop.date is Date" did not work as expected. - else -> iCalObject.dueTimezone = TZ_ALLDAY // prop.date is Date (and not DateTime), therefore it must be Allday - } - } else + is Due<*> -> { + if (iCalObject.component != JtxContract.JtxICalObject.Component.VTODO.name) { logger.warning("The property Due is only supported for VTODO, this value is rejected.") + continue + } + iCalObject.due = prop.normalizedDate().toEpochMilli() + iCalObject.dueTimezone = prop.normalizedDate().getTimeZoneId() } is Duration -> iCalObject.duration = prop.value - is DtStart -> { - iCalObject.dtstart = prop.date.time - when { - prop.date is DateTime && prop.timeZone != null -> iCalObject.dtstartTimezone = prop.timeZone.id - prop.date is DateTime && prop.isUtc -> iCalObject.dtstartTimezone = TimeZone.getTimeZone("UTC").id - prop.date is DateTime && !prop.isUtc && prop.timeZone == null -> iCalObject.dtstartTimezone = null // this comparison is kept on purpose as "prop.date is Date" did not work as expected. - else -> iCalObject.dtstartTimezone = TZ_ALLDAY // prop.date is Date (and not DateTime), therefore it must be Allday - } + is DtStart<*> -> { + iCalObject.dtstart = prop.normalizedDate().toEpochMilli() + iCalObject.dtstartTimezone = prop.normalizedDate().getTimeZoneId() } is PercentComplete -> { @@ -367,84 +439,79 @@ open class JtxICalObject( logger.warning("The property PercentComplete is only supported for VTODO, this value is rejected.") } - is RRule -> iCalObject.rrule = prop.value - is RDate -> { - val rdateList = if(iCalObject.rdate.isNullOrEmpty()) + is RRule<*> -> iCalObject.rrule = prop.value + is RDate<*> -> { + val rdateList: MutableList = if(iCalObject.rdate.isNullOrEmpty()) mutableListOf() else JtxContract.getLongListFromString(iCalObject.rdate!!) - prop.dates.forEach { - rdateList.add(it.time) + prop.normalizedDates().forEach { date -> + date.toEpochMilli()?.let { rdateList.add(it) } } iCalObject.rdate = rdateList.toTypedArray().joinToString(separator = ",") } - is ExDate -> { - val exdateList = if(iCalObject.exdate.isNullOrEmpty()) + is ExDate<*> -> { + val exdateList: MutableList = if(iCalObject.exdate.isNullOrEmpty()) mutableListOf() else JtxContract.getLongListFromString(iCalObject.exdate!!) - prop.dates.forEach { - exdateList.add(it.time) + prop.normalizedDates().forEach { date -> + date.toEpochMilli()?.let { exdateList.add(it) } } iCalObject.exdate = exdateList.toTypedArray().joinToString(separator = ",") } - is RecurrenceId -> { - iCalObject.recurid = prop.date.toString() - iCalObject.recuridTimezone = when { - prop.date is DateTime && prop.timeZone != null -> prop.timeZone.id - prop.date is DateTime && prop.isUtc -> TimeZone.getTimeZone("UTC").id - prop.date is DateTime && !prop.isUtc && prop.timeZone == null -> null - else -> TZ_ALLDAY // prop.date is Date (and not DateTime), therefore it must be Allday - } + is RecurrenceId<*> -> { + iCalObject.recurid = prop.toString() + iCalObject.recuridTimezone = prop.normalizedDate().getTimeZoneId() } //is RequestStatus -> iCalObject.rstatus = prop.value is Categories -> - for (category in prop.categories) + for (category in prop.categories.texts) iCalObject.categories.add(Category(text = category)) is net.fortuna.ical4j.model.property.Comment -> { iCalObject.comments.add( Comment().apply { this.text = prop.value - this.language = prop.parameters?.getParameter(Parameter.LANGUAGE)?.value - this.altrep = prop.parameters?.getParameter(Parameter.ALTREP)?.value + this.language = prop.getParameter(Parameter.LANGUAGE)?.getOrNull()?.value + this.altrep = prop.getParameter(Parameter.ALTREP)?.getOrNull()?.value // remove the known parameter - prop.parameters?.removeAll(Parameter.LANGUAGE) - prop.parameters?.removeAll(Parameter.ALTREP) + prop.parameterList?.removeAll(Parameter.LANGUAGE) + prop.parameterList?.removeAll(Parameter.ALTREP) // save unknown parameters in the other field - this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + this.other = JtxContract.getJsonStringFromXParameters(prop.parameterList) }) } is Resources -> - for (resource in prop.resources) + for (resource in prop.resources.texts) iCalObject.resources.add(Resource(text = resource)) is Attach -> { val attachment = Attachment() prop.uri?.let { attachment.uri = it.toString() } - prop.binary?.let { + prop.binary.array().let { attachment.binary = Base64.encodeToString(it, Base64.DEFAULT) } - prop.parameters?.getParameter(Parameter.FMTTYPE)?.let { + prop.getParameter(Parameter.FMTTYPE)?.getOrNull()?.let { attachment.fmttype = it.value - prop.parameters?.remove(it) + prop.parameterList?.remove(it) } - prop.parameters?.getParameter(X_PARAM_ATTACH_LABEL)?.let { + prop.getParameter(X_PARAM_ATTACH_LABEL)?.getOrNull()?.let { attachment.filename = it.value - prop.parameters.remove(it) + prop.parameterList.remove(it) } - prop.parameters?.getParameter(X_PARAM_FILENAME)?.let { + prop.getParameter(X_PARAM_FILENAME)?.getOrNull()?.let { attachment.filename = it.value - prop.parameters.remove(it) + prop.parameterList.remove(it) } - attachment.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + attachment.other = JtxContract.getJsonStringFromXParameters(prop.parameterList) if (attachment.uri?.isNotEmpty() == true || attachment.binary?.isNotEmpty() == true) // either uri or value must be present! iCalObject.attachments.add(attachment) @@ -455,13 +522,13 @@ open class JtxICalObject( iCalObject.relatedTo.add( RelatedTo().apply { this.text = prop.value - this.reltype = prop.getParameter(RelType.RELTYPE)?.value ?: JtxContract.JtxRelatedto.Reltype.PARENT.name + this.reltype = prop.getParameter(RelType.RELTYPE)?.getOrNull()?.value ?: JtxContract.JtxRelatedto.Reltype.PARENT.name // remove the known parameter - prop.parameters?.removeAll(RelType.RELTYPE) + prop.parameterList?.removeAll(RelType.RELTYPE) // save unknown parameters in the other field - this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + this.other = JtxContract.getJsonStringFromXParameters(prop.parameterList) }) } @@ -469,52 +536,52 @@ open class JtxICalObject( iCalObject.attendees.add( Attendee().apply { this.caladdress = prop.calAddress.toString() - this.cn = prop.parameters?.getParameter(Parameter.CN)?.value - this.delegatedto = prop.parameters?.getParameter(Parameter.DELEGATED_TO)?.value - this.delegatedfrom = prop.parameters?.getParameter(Parameter.DELEGATED_FROM)?.value - this.cutype = prop.parameters?.getParameter(Parameter.CUTYPE)?.value - this.dir = prop.parameters?.getParameter(Parameter.DIR)?.value - this.language = prop.parameters?.getParameter(Parameter.LANGUAGE)?.value - this.member = prop.parameters?.getParameter(Parameter.MEMBER)?.value - this.partstat = prop.parameters?.getParameter(Parameter.PARTSTAT)?.value - this.role = prop.parameters?.getParameter(Parameter.ROLE)?.value - this.rsvp = prop.parameters?.getParameter(Parameter.RSVP)?.value?.toBoolean() - this.sentby = prop.parameters?.getParameter(Parameter.SENT_BY)?.value + this.cn = prop.getParameter(Parameter.CN)?.getOrNull()?.value + this.delegatedto = prop.getParameter(Parameter.DELEGATED_TO)?.getOrNull()?.value + this.delegatedfrom = prop.getParameter(Parameter.DELEGATED_FROM)?.getOrNull()?.value + this.cutype = prop.getParameter(Parameter.CUTYPE)?.getOrNull()?.value + this.dir = prop.getParameter(Parameter.DIR)?.getOrNull()?.value + this.language = prop.getParameter(Parameter.LANGUAGE)?.getOrNull()?.value + this.member = prop.getParameter(Parameter.MEMBER)?.getOrNull()?.value + this.partstat = prop.getParameter(Parameter.PARTSTAT)?.getOrNull()?.value + this.role = prop.getParameter(Parameter.ROLE)?.getOrNull()?.value + this.rsvp = prop.getParameter(Parameter.RSVP)?.getOrNull()?.value?.toBoolean() + this.sentby = prop.getParameter(Parameter.SENT_BY)?.getOrNull()?.value // remove all known parameters so that only unknown parameters remain - prop.parameters?.removeAll(Parameter.CN) - prop.parameters?.removeAll(Parameter.DELEGATED_TO) - prop.parameters?.removeAll(Parameter.DELEGATED_FROM) - prop.parameters?.removeAll(Parameter.CUTYPE) - prop.parameters?.removeAll(Parameter.DIR) - prop.parameters?.removeAll(Parameter.LANGUAGE) - prop.parameters?.removeAll(Parameter.MEMBER) - prop.parameters?.removeAll(Parameter.PARTSTAT) - prop.parameters?.removeAll(Parameter.ROLE) - prop.parameters?.removeAll(Parameter.RSVP) - prop.parameters?.removeAll(Parameter.SENT_BY) + prop.parameterList?.removeAll(Parameter.CN) + prop.parameterList?.removeAll(Parameter.DELEGATED_TO) + prop.parameterList?.removeAll(Parameter.DELEGATED_FROM) + prop.parameterList?.removeAll(Parameter.CUTYPE) + prop.parameterList?.removeAll(Parameter.DIR) + prop.parameterList?.removeAll(Parameter.LANGUAGE) + prop.parameterList?.removeAll(Parameter.MEMBER) + prop.parameterList?.removeAll(Parameter.PARTSTAT) + prop.parameterList?.removeAll(Parameter.ROLE) + prop.parameterList?.removeAll(Parameter.RSVP) + prop.parameterList?.removeAll(Parameter.SENT_BY) // save unknown parameters in the other field - this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + this.other = JtxContract.getJsonStringFromXParameters(prop.parameterList) } ) } is net.fortuna.ical4j.model.property.Organizer -> { iCalObject.organizer = Organizer().apply { this.caladdress = prop.calAddress.toString() - this.cn = prop.parameters?.getParameter(Parameter.CN)?.value - this.dir = prop.parameters?.getParameter(Parameter.DIR)?.value - this.language = prop.parameters?.getParameter(Parameter.LANGUAGE)?.value - this.sentby = prop.parameters?.getParameter(Parameter.SENT_BY)?.value + this.cn = prop.getParameter(Parameter.CN)?.getOrNull()?.value + this.dir = prop.getParameter(Parameter.DIR)?.getOrNull()?.value + this.language = prop.getParameter(Parameter.LANGUAGE)?.getOrNull()?.value + this.sentby = prop.getParameter(Parameter.SENT_BY)?.getOrNull()?.value // remove all known parameters so that only unknown parameters remain - prop.parameters?.removeAll(Parameter.CN) - prop.parameters?.removeAll(Parameter.DIR) - prop.parameters?.removeAll(Parameter.LANGUAGE) - prop.parameters?.removeAll(Parameter.SENT_BY) + prop.parameterList?.removeAll(Parameter.CN) + prop.parameterList?.removeAll(Parameter.DIR) + prop.parameterList?.removeAll(Parameter.LANGUAGE) + prop.parameterList?.removeAll(Parameter.SENT_BY) // save unknown parameters in the other field - this.other = JtxContract.getJsonStringFromXParameters(prop.parameters) + this.other = JtxContract.getJsonStringFromXParameters(prop.parameterList) } } @@ -553,7 +620,33 @@ open class JtxICalObject( if (iCalObject.duration != null && iCalObject.dtstart == null) { logger.warning("Found DURATION without DTSTART; ignoring") iCalObject.duration = null - }*/ + } + } + private fun Temporal.toEpochMilli(): Long? = when (this) { + is ZonedDateTime -> this.toInstant().toEpochMilli() // Calculate from contained time zone + is Instant -> this.toEpochMilli() // Calculated from UTC time + is LocalDateTime -> this + .atZone(ZoneId.systemDefault()) // Use system default time zone to interpret as local time + .toInstant() + .toEpochMilli() + is LocalDate -> this + .atStartOfDay(ZoneOffset.UTC) // Use start of day for local date without time (ie. local all-day events) + .toInstant() + .toEpochMilli() + else -> { + logger.warning("Ignoring unsupported temporal type: ${this::class}") + null + } + } + private fun Temporal.getTimeZoneId(): String? = when (this) { + is ZonedDateTime -> this.zone.id // We got a timezone + is Instant -> ZoneOffset.UTC.id // Instant is a point on the UTC timeline + is LocalDateTime -> null // Timezone unknown => floating time + is LocalDate -> TZ_ALLDAY // Without time, it is considered all-day + else -> { + logger.warning("Ignoring unsupported temporal type: ${this::class}") + null + } } } @@ -566,27 +659,26 @@ open class JtxICalObject( */ fun getICalendarFormat(prodId: ProdId): Calendar? { val ical = Calendar() - TODO("ical4j 4.x") - /*ical.properties += Version.VERSION_2_0 - ical.properties += prodId.withUserAgents(listOf(TaskProvider.ProviderName.JtxBoard.packageName)) + ical += ImmutableVersion.VERSION_2_0 + ical += prodId.withUserAgents(listOf(TaskProvider.ProviderName.JtxBoard.packageName)) val calComponent = when (component) { JtxContract.JtxICalObject.Component.VTODO.name -> VToDo(true /* generates DTSTAMP */) JtxContract.JtxICalObject.Component.VJOURNAL.name -> VJournal(true /* generates DTSTAMP */) else -> return null } - ical.components += calComponent - addProperties(calComponent.properties) + ical += calComponent + addProperties(calComponent.propertyList) alarms.forEach { alarm -> val vAlarm = VAlarm() - vAlarm.properties.apply { + vAlarm.propertyList.apply { alarm.action?.let { when (it) { - JtxContract.JtxAlarm.AlarmAction.DISPLAY.name -> add(Action.DISPLAY) - JtxContract.JtxAlarm.AlarmAction.AUDIO.name -> add(Action.AUDIO) - JtxContract.JtxAlarm.AlarmAction.EMAIL.name -> add(Action.EMAIL) + JtxContract.JtxAlarm.AlarmAction.DISPLAY.name -> add(ImmutableAction.DISPLAY) + JtxContract.JtxAlarm.AlarmAction.AUDIO.name -> add(ImmutableAction.AUDIO) + JtxContract.JtxAlarm.AlarmAction.EMAIL.name -> add(ImmutableAction.EMAIL) else -> return@let } } @@ -599,9 +691,9 @@ open class JtxICalObject( // Add the RELATED parameter if present alarm.triggerRelativeTo?.let { if(it == JtxContract.JtxAlarm.AlarmRelativeTo.START.name) - this.parameters.add(Related.START) + this.parameterList.add(Related.START) if(it == JtxContract.JtxAlarm.AlarmRelativeTo.END.name) - this.parameters.add(Related.END) + this.parameterList.add(Related.END) } } catch (e: DateTimeParseException) { logger.log(Level.WARNING, "Could not parse Trigger duration as Duration.", e) @@ -612,18 +704,11 @@ open class JtxICalObject( add(Trigger().apply { try { when { - alarm.triggerTimezone == TimeZone.getTimeZone("UTC").id -> this.dateTime = DateTime(alarm.triggerTime!!).apply { - this.isUtc = true - } - alarm.triggerTimezone.isNullOrEmpty() -> this.dateTime = DateTime(alarm.triggerTime!!).apply { - this.isUtc = true - } + alarm.triggerTimezone == ZoneOffset.UTC.id || + alarm.triggerTimezone.isNullOrEmpty() -> + this.date = Instant.ofEpochMilli(alarm.triggerTime!!) else -> { - val timezone = TimeZoneRegistryFactory.getInstance().createRegistry() - .getTimeZone(alarm.triggerTimezone) - this.dateTime = DateTime(alarm.triggerTime!!).apply{ - this.timeZone = timezone - } + this.date = ZonedDateTime.ofInstant(Instant.ofEpochMilli(alarm.triggerTime!!), ZoneId.of(alarm.triggerTimezone)).toInstant() } } } catch (e: ParseException) { @@ -642,10 +727,10 @@ open class JtxICalObject( }) } alarm.description?.let { add(Description(it)) } alarm.attach?.let { add(Attach().apply { value = it }) } - alarm.other?.let { addAll(JtxContract.getXPropertyListFromJson(it)) } + alarm.other?.let { addAll(JtxContract.getXPropertyListFromJson(it).all) } } - calComponent.components.add(vAlarm) + calComponent.componentList.add(vAlarm) } @@ -655,11 +740,11 @@ open class JtxICalObject( JtxContract.JtxICalObject.Component.VJOURNAL.name -> VJournal(true /* generates DTSTAMP */) else -> return null } - ical.components += recurCalComponent - recurInstance.addProperties(recurCalComponent.properties) + ical += recurCalComponent + recurInstance.addProperties(recurCalComponent.propertyList) } - ICalendar.softValidate(ical)*/ + ICalendar.softValidate(ical) return ical } @@ -678,16 +763,11 @@ open class JtxICalObject( * @param [props] The PropertyList where the properties should be added */ private fun addProperties(props: PropertyList) { - TODO("ical4j 4.x") - /*uid.let { props += Uid(it) } + uid.let { props += Uid(it) } sequence.let { props += Sequence(it.toInt()) } - created.let { props += Created(DateTime(it).apply { - this.isUtc = true - }) } - lastModified.let { props += LastModified(DateTime(it).apply { - this.isUtc = true - }) } + created.let { props += Created(Instant.ofEpochMilli(it)) } + lastModified.let { props += LastModified(Instant.ofEpochMilli(it))} summary.let { props += Summary(it) } description?.let { props += Description(it) } @@ -695,7 +775,7 @@ open class JtxICalObject( location?.let { location -> val loc = Location(location) locationAltrep?.let { locationAltrep -> - loc.parameters.add(AltRep(locationAltrep)) + loc.parameterList.add(AltRep(locationAltrep)) } props += loc } @@ -725,7 +805,7 @@ open class JtxICalObject( categories.forEach { categoryTextList.add(it.text) } - if (!categoryTextList.isEmpty) + if (!categoryTextList.texts.isEmpty()) props += Categories(categoryTextList) @@ -733,18 +813,18 @@ open class JtxICalObject( resources.forEach { resourceTextList.add(it.text) } - if (!resourceTextList.isEmpty) - props += Resources(resourceTextList) + if (!resourceTextList.texts.isEmpty()) + props += Resources(resourceTextList.texts.toList()) comments.forEach { comment -> - val c = Comment(comment.text).apply { - comment.altrep?.let { this.parameters.add(AltRep(it)) } - comment.language?.let { this.parameters.add(Language(it)) } + val c = net.fortuna.ical4j.model.property.Comment(comment.text).apply { + comment.altrep?.let { this.parameterList.add(AltRep(it)) } + comment.language?.let { this.parameterList.add(Language(it)) } comment.other?.let { val xparams = JtxContract.getXParametersFromJson(it) xparams.forEach { xparam -> - this.parameters.add(xparam) + this.parameterList.add(xparam) } } } @@ -757,49 +837,49 @@ open class JtxICalObject( this.calAddress = URI(attendee.caladdress) attendee.cn?.let { - this.parameters.add(Cn(it)) + this.parameterList.add(Cn(it)) } attendee.cutype?.let { when { - it.equals(CuType.INDIVIDUAL.value, ignoreCase = true) -> this.parameters.add(CuType.INDIVIDUAL) - it.equals(CuType.GROUP.value, ignoreCase = true) -> this.parameters.add(CuType.GROUP) - it.equals(CuType.ROOM.value, ignoreCase = true) -> this.parameters.add(CuType.ROOM) - it.equals(CuType.RESOURCE.value, ignoreCase = true) -> this.parameters.add(CuType.RESOURCE) - it.equals(CuType.UNKNOWN.value, ignoreCase = true) -> this.parameters.add(CuType.UNKNOWN) - else -> this.parameters.add(CuType.UNKNOWN) + it.equals(CuType.INDIVIDUAL.value, ignoreCase = true) -> this.parameterList.add(CuType.INDIVIDUAL) + it.equals(CuType.GROUP.value, ignoreCase = true) -> this.parameterList.add(CuType.GROUP) + it.equals(CuType.ROOM.value, ignoreCase = true) -> this.parameterList.add(CuType.ROOM) + it.equals(CuType.RESOURCE.value, ignoreCase = true) -> this.parameterList.add(CuType.RESOURCE) + it.equals(CuType.UNKNOWN.value, ignoreCase = true) -> this.parameterList.add(CuType.UNKNOWN) + else -> this.parameterList.add(CuType.UNKNOWN) } } attendee.delegatedfrom?.let { - this.parameters.add(DelegatedFrom(it)) + this.parameterList.add(DelegatedFrom(it)) } attendee.delegatedto?.let { - this.parameters.add(DelegatedTo(it)) + this.parameterList.add(DelegatedTo(it)) } attendee.dir?.let { - this.parameters.add(Dir(it)) + this.parameterList.add(Dir(it)) } attendee.language?.let { - this.parameters.add(Language(it)) + this.parameterList.add(Language(it)) } attendee.member?.let { - this.parameters.add(Member(it)) + this.parameterList.add(Member(it)) } attendee.partstat?.let { - this.parameters.add(PartStat(it)) + this.parameterList.add(PartStat(it)) } attendee.role?.let { - this.parameters.add(Role(it)) + this.parameterList.add(Role(it)) } attendee.rsvp?.let { - this.parameters.add(Rsvp(it)) + this.parameterList.add(Rsvp(it)) } attendee.sentby?.let { - this.parameters.add(SentBy(it)) + this.parameterList.add(SentBy(it)) } attendee.other?.let { val params = JtxContract.getXParametersFromJson(it) params.forEach { xparam -> - this.parameters.add(xparam) + this.parameterList.add(xparam) } } } @@ -812,21 +892,21 @@ open class JtxICalObject( this.calAddress = URI(organizer.caladdress) organizer.cn?.let { - this.parameters.add(Cn(it)) + this.parameterList.add(Cn(it)) } organizer.dir?.let { - this.parameters.add(Dir(it)) + this.parameterList.add(Dir(it)) } organizer.language?.let { - this.parameters.add(Language(it)) + this.parameterList.add(Language(it)) } organizer.sentby?.let { - this.parameters.add(SentBy(it)) + this.parameterList.add(SentBy(it)) } organizer.other?.let { val params = JtxContract.getXParametersFromJson(it) params.forEach { xparam -> - this.parameters.add(xparam) + this.parameterList.add(xparam) } } } @@ -840,12 +920,12 @@ open class JtxICalObject( val attachmentUri = ContentUris.withAppendedId(JtxContract.JtxAttachment.CONTENT_URI.asSyncAdapter(collection.account), attachment.attachmentId) val attachmentFile = collection.client.openFile(attachmentUri, "r") - val attachmentBytes = ParcelFileDescriptor.AutoCloseInputStream(attachmentFile).readBytes() + val attachmentBytes = ByteBuffer.wrap(ParcelFileDescriptor.AutoCloseInputStream(attachmentFile).readBytes()) val att = Attach(attachmentBytes).apply { - attachment.fmttype?.let { this.parameters.add(FmtType(it)) } + attachment.fmttype?.let { this.parameterList.add(FmtType(it)) } attachment.filename?.let { - this.parameters.add(XParameter(X_PARAM_ATTACH_LABEL, it)) - this.parameters.add(XParameter(X_PARAM_FILENAME, it)) + this.parameterList.add(XParameter(X_PARAM_ATTACH_LABEL, it)) + this.parameterList.add(XParameter(X_PARAM_FILENAME, it)) } } props += att @@ -853,10 +933,10 @@ open class JtxICalObject( } else { attachment.uri?.let { uri -> val att = Attach(URI(uri)).apply { - attachment.fmttype?.let { this.parameters.add(FmtType(it)) } + attachment.fmttype?.let { this.parameterList.add(FmtType(it)) } attachment.filename?.let { - this.parameters.add(XParameter(X_PARAM_ATTACH_LABEL, it)) - this.parameters.add(XParameter(X_PARAM_FILENAME, it)) + this.parameterList.add(XParameter(X_PARAM_ATTACH_LABEL, it)) + this.parameterList.add(XParameter(X_PARAM_FILENAME, it)) } } props += att @@ -887,78 +967,60 @@ open class JtxICalObject( } val parameterList = ParameterList() parameterList.add(param) - props += RelatedTo(parameterList, it.text) + props += net.fortuna.ical4j.model.property.RelatedTo(parameterList, it.text) } dtstart?.let { - when { - dtstartTimezone == TZ_ALLDAY -> props += DtStart(Date(it)) - dtstartTimezone == TimeZone.getTimeZone("UTC").id -> props += DtStart(DateTime(it).apply { - this.isUtc = true - }) - dtstartTimezone.isNullOrEmpty() -> props += DtStart(DateTime(it).apply { - this.isUtc = false - }) - else -> { - val timezone = TimeZoneRegistryFactory.getInstance().createRegistry() - .getTimeZone(dtstartTimezone) - val withTimezone = DtStart(DateTime(it)) - withTimezone.timeZone = timezone - props += withTimezone - } + props += if (dtstartTimezone == TZ_ALLDAY || // allday uses UTC + dtstartTimezone.isNullOrEmpty() || // floating time -> use UTC to calculate instant + dtstartTimezone == ZoneOffset.UTC.id // UTC -> TZID=UTC + ) { + DtStart(Instant.ofEpochMilli(it)) + } else { + DtStart(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.of(dtstartTimezone))) } } rrule?.let { rrule -> - props += RRule(rrule) + props += RRule(rrule) } recurid?.let { recurid -> - props += when { - recuridTimezone == TZ_ALLDAY -> RecurrenceId(Date(recurid)) - recuridTimezone == TimeZone.getTimeZone("UTC").id -> RecurrenceId(DateTime(recurid).apply { this.isUtc = true }) - recuridTimezone.isNullOrEmpty() -> RecurrenceId(DateTime(recurid).apply { this.isUtc = false }) - else -> RecurrenceId(DateTime(recurid, TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(recuridTimezone))) - } + props += if (recuridTimezone == TZ_ALLDAY || recuridTimezone.isNullOrEmpty()) + RecurrenceId(recurid) + else + RecurrenceId(ParameterList(listOf(TzId(recuridTimezone))), recurid) } rdate?.let { rdateString -> when { dtstartTimezone == TZ_ALLDAY -> { - val dateListDate = DateList(Value.DATE) + val localDates = DateList() JtxContract.getLongListFromString(rdateString).forEach { - dateListDate.add(Date(it)) + localDates.add(LocalDate.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)) } - props += RDate(dateListDate) - + props += RDate(localDates) } - dtstartTimezone == TimeZone.getTimeZone("UTC").id -> { - val dateListDateTime = DateList(Value.DATE_TIME) + dtstartTimezone == ZoneOffset.UTC.id -> { + val zonedDateTimes = DateList() JtxContract.getLongListFromString(rdateString).forEach { - dateListDateTime.add(DateTime(it).apply { - this.isUtc = true - }) + zonedDateTimes.add(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)) } - props += RDate(dateListDateTime) + props += RDate(zonedDateTimes) } dtstartTimezone.isNullOrEmpty() -> { - val dateListDateTime = DateList(Value.DATE_TIME) + val localDateTimes = DateList() JtxContract.getLongListFromString(rdateString).forEach { - dateListDateTime.add(DateTime(it).apply { - this.isUtc = false - }) + localDateTimes.add(LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())) } - props += RDate(dateListDateTime) + props += RDate(localDateTimes) } else -> { - val dateListDateTime = DateList(Value.DATE_TIME) - val timezone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(dtstartTimezone) + val zonedDateTimes = DateList() JtxContract.getLongListFromString(rdateString).forEach { - val withTimezone = DateTime(it) - withTimezone.timeZone = timezone - dateListDateTime.add(DateTime(withTimezone)) + zonedDateTimes.add(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.of(dtstartTimezone))) } - props += RDate(dateListDateTime) + props += RDate(zonedDateTimes) } } } @@ -967,40 +1029,32 @@ open class JtxICalObject( when { dtstartTimezone == TZ_ALLDAY -> { - val dateListDate = DateList(Value.DATE) + val localDates = DateList() JtxContract.getLongListFromString(exdateString).forEach { - dateListDate.add(Date(it)) + localDates.add(LocalDate.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)) } - props += ExDate(dateListDate) - + props += ExDate(localDates) } - dtstartTimezone == TimeZone.getTimeZone("UTC").id -> { - val dateListDateTime = DateList(Value.DATE_TIME) + dtstartTimezone == ZoneOffset.UTC.id -> { + val zonedDateTimes = DateList() JtxContract.getLongListFromString(exdateString).forEach { - dateListDateTime.add(DateTime(it).apply { - this.isUtc = true - }) + zonedDateTimes.add(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)) } - props += ExDate(dateListDateTime) + props += ExDate(zonedDateTimes) } dtstartTimezone.isNullOrEmpty() -> { - val dateListDateTime = DateList(Value.DATE_TIME) + val localDateTimes = DateList() JtxContract.getLongListFromString(exdateString).forEach { - dateListDateTime.add(DateTime(it).apply { - this.isUtc = false - }) + localDateTimes.add(LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())) } - props += ExDate(dateListDateTime) + props += ExDate(localDateTimes) } else -> { - val dateListDateTime = DateList(Value.DATE_TIME) - val timezone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(dtstartTimezone) + val zonedDateTimes = DateList() JtxContract.getLongListFromString(exdateString).forEach { - val withTimezone = DateTime(it) - withTimezone.timeZone = timezone - dateListDateTime.add(DateTime(withTimezone)) + zonedDateTimes.add(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.of(dtstartTimezone))) } - props += ExDate(dateListDateTime) + props += ExDate(zonedDateTimes) } } } @@ -1021,10 +1075,10 @@ duration?.let(props::add) if(component == JtxContract.JtxICalObject.Component.VTODO.name) { completed?.let { - //Completed is defines as always DateTime! And is always UTC! But the X_PROP_COMPLETEDTIMEZONE can still define a timezone - props += Completed(DateTime(it)) + // Completed is UNIX timestamp (milliseconds). But the X_PROP_COMPLETEDTIMEZONE can still define a timezone + props += Completed(Instant.ofEpochMilli(it)) - // only take completedTimezone if there was the completed time set + // only take completedTimezone if completed time is set completedTimezone?.let { complTZ -> props += XProperty(X_PROP_COMPLETEDTIMEZONE, complTZ) } @@ -1035,34 +1089,26 @@ duration?.let(props::add) } - if (priority != null && priority != Priority.UNDEFINED.level) + if (priority != null && priority != ImmutablePriority.UNDEFINED.level) priority?.let { props += Priority(it) } else { - props += Priority(Priority.UNDEFINED.level) + props += Priority(ImmutablePriority.UNDEFINED.level) } due?.let { - when { - dueTimezone == TZ_ALLDAY -> props += Due(Date(it)) - dueTimezone == TimeZone.getTimeZone("UTC").id -> props += Due(DateTime(it).apply { - this.isUtc = true - }) - dueTimezone.isNullOrEmpty() -> props += Due(DateTime(it).apply { - this.isUtc = false - }) - else -> { - val timezone = TimeZoneRegistryFactory.getInstance().createRegistry() - .getTimeZone(dueTimezone) - val withTimezone = Due(DateTime(it)) - withTimezone.timeZone = timezone - props += withTimezone - } + props += when { + dueTimezone == TZ_ALLDAY -> Due(LocalDate.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)) + dueTimezone == ZoneOffset.UTC.id -> Due(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)) + dueTimezone.isNullOrEmpty() -> Due(LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())) + else -> Due(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.of(dueTimezone))) } } } + } + /* // determine earliest referenced date val earliest = arrayOf( dtStart?.date, @@ -1072,9 +1118,7 @@ duration?.let(props::add) // add VTIMEZONE components for (tz in usedTimeZones) ical.components += ICalendar.minifyVTimeZone(tz.vTimeZone, earliest) - - TODO */ - } + } */ fun prepareForUpload(): String { @@ -1117,8 +1161,7 @@ duration?.let(props::add) * @return the Content [Uri] of the inserted object */ fun add(): Uri { - TODO("ical4j 4.x") - /*val values = this.toContentValues() + val values = this.toContentValues() val newUri = collection.client.insert( JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account), @@ -1128,7 +1171,7 @@ duration?.let(props::add) insertOrUpdateListProperties(false) - return newUri*/ + return newUri } /** @@ -1137,8 +1180,7 @@ duration?.let(props::add) * @return [Uri] of the updated entry */ fun update(data: JtxICalObject): Uri { - TODO("ical4j 4.x") - /*this.applyNewData(data) + this.applyNewData(data) val values = this.toContentValues() var updateUri = JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(collection.account) @@ -1152,7 +1194,7 @@ duration?.let(props::add) insertOrUpdateListProperties(true) - return updateUri*/ + return updateUri } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt index fc037b3c..fdb35915 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/DatePropertyTzMapper.kt @@ -7,8 +7,10 @@ package at.bitfire.synctools.icalendar import androidx.annotation.VisibleForTesting +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.parameter.TzId +import net.fortuna.ical4j.model.property.DateListProperty import net.fortuna.ical4j.model.property.DateProperty import java.time.OffsetDateTime import java.time.ZoneId @@ -60,6 +62,50 @@ object DatePropertyTzMapper { } } + /** + * Normalizes the date properties to a system-compatible temporal representation. Just like + * [normalizedDate], but for date lists. + * + * Processes the underlying dates or date-times of the DateListProperty to ensure compatibility + * with content providers by converting ical4j-specific temporal types to system-known types: + * + * - Converts OffsetDateTime to Instant (UTC timestamp). + * - Converts ZonedDateTime with ical4j-based timezones to ZonedDateTime with system-known ZoneId. + * - Leaves Instant, LocalDate, and other temporal types unchanged. + * + * @see normalizedDate + * + * @return A list of normalized Temporal objects: + * - Instant for UTC date-times (originally OffsetDateTime). + * - ZonedDateTime with system-known ZoneId for date-times with TZID. + * - Original Temporal type for other cases (Instant, LocalDate, etc.). + */ + fun DateListProperty<*>.normalizedDates(): List { + /* These dates are generated by ical4j's TemporalAdapter and use + - OffsetDateTime for UTC date-times ("...Z") and + - ZonedDateTime with ical4j-based timezones for date-times with TZID. */ + val origDates: List = dates + + return origDates.map { origDate -> + when (origDate) { + // In content providers, there's no concept of offset date-times. We just want the UTC timestamp instead. + is OffsetDateTime -> + origDate.toInstant() + + // In case of date-time with TZID, make sure that we have a ZonedDateTime with a system-known ZoneId. + is ZonedDateTime -> + normalizeZonedDateTime( + origDate = origDate, + tzId = getParameter(Parameter.TZID).getOrNull()?.value + ) + + else -> + // return Instant and LocalDate/... as it is + origDate + } + } + } + private fun normalizeZonedDateTime(origDate: ZonedDateTime, tzId: String?): ZonedDateTime { /* In case of ZonedDateTime, date.zone.id will look like "ical4j~" (if taken from ical4j database) or "ical4j-local-xxx" (if generated from VTIMEZONE). We want to diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt index 3670fb0e..3a79d573 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt @@ -58,6 +58,10 @@ operator fun PropertyContainer.plusAssign(property: Property) { add(property) } +operator fun PropertyList.plusAssign(property: Property) { + add(property) +} + operator fun ComponentContainer.plusAssign(component: T) { add>(component) } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt index 2059e5cd..cef1c19a 100644 --- a/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt +++ b/lib/src/main/kotlin/at/techbee/jtx/JtxContract.kt @@ -41,7 +41,6 @@ import java.util.logging.Level import java.util.logging.Logger -@Suppress("unused") object JtxContract { private val logger @@ -141,12 +140,12 @@ object JtxContract { return null val jsonObject = JSONObject() - TODO("ical4j 4.x") + // Note: probably the contract should be separated from methods that do things, especially if they depend on ical4j - /*parameters.forEach { parameter -> + parameters.all.forEach { parameter -> jsonObject.put(parameter.name, parameter.value) - }*/ + } return if (jsonObject.length() == 0) null else @@ -163,17 +162,16 @@ object JtxContract { if (propertyList == null) return null - TODO("ical4j 4.x") // Note: probably the contract should be separated from methods that do things, especially if they depend on ical4j - /*val jsonObject = JSONObject() - propertyList.forEach { property -> + val jsonObject = JSONObject() + propertyList.all.forEach { property -> jsonObject.put(property.name, property.value) } return if (jsonObject.length() == 0) null else - jsonObject.toString()*/ + jsonObject.toString() } @@ -192,7 +190,7 @@ object JtxContract { stringList.forEach { try { longList.add(it.toLong()) - } catch (e: NumberFormatException) { + } catch (_: NumberFormatException) { logger.log(Level.WARNING, "String could not be cast to Long ($it)") return@forEach } @@ -201,7 +199,6 @@ object JtxContract { } - @Suppress("unused") object JtxICalObject { /** The name of the the content URI for IcalObjects. @@ -659,7 +656,6 @@ object JtxContract { } - @Suppress("unused") object JtxAttendee { /** The name of the the table for Attendees that are linked to an ICalObject. @@ -818,19 +814,16 @@ object JtxContract { } /** This enum class defines the possible values for the attribute [JtxAttendee] for the Component VJOURNAL */ - @Suppress("unused") enum class PartstatJournal { `NEEDS-ACTION`, ACCEPTED, DECLINED } /** This enum class defines the possible values for the attribute [JtxAttendee] for the Component VTODO */ - @Suppress("unused") enum class PartstatTodo { `NEEDS-ACTION`, ACCEPTED, DECLINED, TENTATIVE, DELEGATED, COMPLETED, `IN-PROCESS` } } - @Suppress("unused") object JtxCategory { /** The name of the the table for Categories that are linked to an ICalObject. @@ -880,7 +873,7 @@ object JtxContract { const val OTHER = "other" } - @Suppress("unused") + object JtxComment { /** The name of the the table for Comments that are linked to an ICalObject. @@ -938,7 +931,7 @@ object JtxContract { } - @Suppress("unused") + object JtxOrganizer { /** The name of the the table for Organizer that are linked to an ICalObject. * [https://tools.ietf.org/html/rfc5545#section-3.8.4.3] @@ -1017,7 +1010,7 @@ object JtxContract { } - @Suppress("unused") + object JtxRelatedto { /** The name of the the table for Relationships (related-to) that are linked to an ICalObject. @@ -1087,7 +1080,7 @@ object JtxContract { } - @Suppress("unused") + object JtxResource { /** The name of the the table for Resources that are linked to an ICalObject. * [https://tools.ietf.org/html/rfc5545#section-3.8.1.10]*/ @@ -1137,7 +1130,7 @@ object JtxContract { } - @Suppress("unused") + object JtxCollection { /** The name of the the table for Collections @@ -1261,7 +1254,7 @@ object JtxContract { } - @Suppress("unused") + object JtxAttachment { /** The name of the the table for Attachments that are linked to an ICalObject.*/ @@ -1325,7 +1318,7 @@ object JtxContract { } - @Suppress("unused") + object JtxAlarm { /** The name of the the table for Alarms that are linked to an ICalObject.*/ @@ -1459,19 +1452,17 @@ object JtxContract { const val TRIGGER_RELATIVE_DURATION = "triggerRelativeDuration" /** This enum class defines the possible values for the attribute [TRIGGER_RELATIVE_TO] for the Component VALARM */ - @Suppress("unused") enum class AlarmRelativeTo { START, END } /** This enum class defines the possible values for the attribute [ACTION] for the Component VALARM */ - @Suppress("unused") enum class AlarmAction { AUDIO, DISPLAY, EMAIL } } - @Suppress("unused") + object JtxUnknown { /** The name of the the table for Unknown properties that are linked to an ICalObject.*/ From 7ae50e9ca62196057d764c9670686f2642b25942 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 16 Mar 2026 13:28:24 +0100 Subject: [PATCH 12/24] [ical4j 4.x] Update task property mapping (#218) * Update tasks mapping for ical4j 4.x Signed-off-by: Arnau Mora * Remove TODO Signed-off-by: Arnau Mora * Typos Signed-off-by: Arnau Mora * Fix missing annotation Signed-off-by: Arnau Mora * [wip] fix tests for androidifyTimeZone (date) Signed-off-by: Arnau Mora * Rollback Signed-off-by: Arnau Mora * bis-Rollback Signed-off-by: Arnau Mora * Change implementation Signed-off-by: Arnau Mora * Update implementation Signed-off-by: Arnau Mora * Update DmfsTaskTest Signed-off-by: Arnau Mora * Add DatePropertyTzMapper to normalize ical4j timezones to system timezones * Deprecate timezone utilities in favor of DatePropertyTzMapper * Make systemTzId visible for testing and add unit tests * Remove deprecated AndroidTimeUtils tests for ical4j 4.x compatibility * Refactor DatePropertyTzMapper to handle OffsetDateTime and improve normalization - Convert OffsetDateTime to Instant for UTC timestamps - Extract ZonedDateTime normalization logic into separate method - Add tests for Instant, LocalDate, and OffsetDateTime handling - Improve documentation and method signatures * Fix DatePropertyTzMapperTest to use system default timezone * Implement recurrenceSetsToOpenTasksString Signed-off-by: Arnau Mora * Missing import Signed-off-by: Arnau Mora * Fix comments Signed-off-by: Arnau Mora --------- Signed-off-by: Arnau Mora Co-authored-by: Ricki Hirner --- .../at/bitfire/ical4android/DmfsTaskTest.kt | 46 ++- .../mapping/tasks/DmfsTaskBuilderTest.kt | 214 +++++++---- .../mapping/tasks/DmfsTaskProcessorTest.kt | 337 ++++++++++++++++++ .../AndroidCompatTimeZoneRegistry.kt | 26 +- .../kotlin/at/bitfire/ical4android/Task.kt | 6 +- .../at/bitfire/ical4android/util/DateUtils.kt | 48 ++- .../mapping/tasks/DmfsTaskBuilder.kt | 106 +++--- .../mapping/tasks/DmfsTaskProcessor.kt | 122 +++---- .../synctools/util/AndroidTimeUtils.kt | 65 +++- .../synctools/util/AndroidTimeUtilsTest.kt | 65 ++-- 10 files changed, 772 insertions(+), 263 deletions(-) create mode 100644 lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessorTest.kt diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt index 4ae45c94..cf29b339 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskTest.kt @@ -13,7 +13,6 @@ import androidx.core.content.contentValuesOf import at.bitfire.ical4android.impl.TestTaskList import at.bitfire.synctools.storage.LocalStorageException import at.bitfire.synctools.storage.tasks.DmfsTaskList -import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.RelType @@ -31,6 +30,8 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Test +import java.time.LocalDate +import java.time.ZonedDateTime class DmfsTaskTest( providerName: TaskProvider.ProviderName @@ -82,14 +83,21 @@ class DmfsTaskTest( @Test fun testAddTask() { // build and write event to calendar provider - TODO("ical4j 4.x") - /*val task = Task() + val task = Task() task.uid = "sample1@testAddEvent" task.summary = "Sample event" task.description = "Sample event with date/time" task.location = "Sample location" - task.dtStart = DtStart("20150501T120000", tzVienna) - task.due = Due("20150501T140000", tzVienna) + task.dtStart = DtStart(ZonedDateTime.of( + 2015, 5, 1, + 12, 0, 0, 0, + tzVienna.toZoneId() + )) + task.due = Due(ZonedDateTime.of( + 2015, 5, 1, + 14, 0, 0, 0, + tzVienna.toZoneId() + )) task.organizer = Organizer("mailto:organizer@example.com") assertFalse(task.isAllDay()) @@ -97,9 +105,10 @@ class DmfsTaskTest( task.categories.addAll(arrayOf("Cat1", "Cat2")) task.comment = "A comment" - val sibling = RelatedTo("most-fields2@example.com") - sibling.parameters.add(RelType.SIBLING) - task.relatedTo.add(sibling) + task.relatedTo.add( + RelatedTo("most-fields2@example.com") + .add(RelType.SIBLING) + ) task.unknownProperties += XProperty("X-UNKNOWN-PROP", "Unknown Value") @@ -126,19 +135,18 @@ class DmfsTaskTest( assertEquals(task.unknownProperties, task2.unknownProperties) } finally { testTask.delete() - }*/ + } } @Test(expected = LocalStorageException::class) fun testAddTaskWithInvalidDue() { val task = Task() - TODO("ical4j 4.x") - /*task.uid = "invalidDUE@ical4android.tests" + task.uid = "invalidDUE@ical4android.tests" task.summary = "Task with invalid DUE" - task.dtStart = DtStart(Date("20150102")) + task.dtStart = DtStart(LocalDate.of(2015, 1, 2)) - task.due = Due(Date("20150101")) - DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add()*/ + task.due = Due(LocalDate.of(2015, 1, 1)) + DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() } @Test @@ -146,8 +154,7 @@ class DmfsTaskTest( val task = Task() task.uid = "TaskWithManyAlarms" task.summary = "Task with many alarms" - TODO("ical4j 4.x") - //task.dtStart = DtStart(Date("20150102")) + task.dtStart = DtStart(LocalDate.of(2015, 1, 2)) for (i in 1..1050) task.alarms += VAlarm(java.time.Duration.ofMinutes(i.toLong())) @@ -165,8 +172,11 @@ class DmfsTaskTest( task.summary = "Sample event" task.description = "Sample event with date/time" task.location = "Sample location" - TODO("ical4j 4.x") - //task.dtStart = DtStart("20150501T120000", tzVienna) + task.dtStart = DtStart(ZonedDateTime.of( + 2015, 5, 1, + 12, 0, 0, 0, + tzVienna.toZoneId() + )) assertFalse(task.isAllDay()) val uri = DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() assertNotNull(uri) diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt index 497a3d42..c57e6d4c 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilderTest.kt @@ -18,15 +18,13 @@ import at.bitfire.ical4android.Task import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.UnknownProperty import at.bitfire.ical4android.impl.TestTaskList +import at.bitfire.ical4android.util.DateUtils.toEpochMilli import at.bitfire.synctools.storage.tasks.DmfsTaskList -import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.TzId -import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.Clazz import net.fortuna.ical4j.model.property.Completed @@ -41,16 +39,23 @@ import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RelatedTo import net.fortuna.ical4j.model.property.Status import net.fortuna.ical4j.model.property.XProperty +import net.fortuna.ical4j.model.property.immutable.ImmutableClazz +import net.fortuna.ical4j.model.property.immutable.ImmutableStatus import org.dmfs.tasks.contract.TaskContract import org.junit.After import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Ignore import org.junit.Test +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.temporal.Temporal -@Ignore("ical4j 4.x") class DmfsTaskBuilderTest ( providerName: TaskProvider.ProviderName ): DmfsStyleProvidersTaskTest(providerName) { @@ -84,11 +89,7 @@ class DmfsTaskBuilderTest ( // builder tests - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun testBuildTask_Sequence() { buildTask { ICalendar.apply { sequence = 12345 } @@ -188,7 +189,7 @@ class DmfsTaskBuilderTest ( fun testBuildTask_Organizer_EmailParameter() { buildTask { organizer = Organizer("uri:unknown").apply { - parameters.add(Email("organizer@example.com")) + add(Email("organizer@example.com")) } }.let { result -> assertEquals( @@ -219,7 +220,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Classification_Public() { buildTask { - classification = Clazz.PUBLIC + classification = Clazz(ImmutableClazz.VALUE_PUBLIC) }.let { result -> assertEquals( TaskContract.Tasks.CLASSIFICATION_PUBLIC, @@ -231,7 +232,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Classification_Private() { buildTask { - classification = Clazz.PRIVATE + classification = Clazz(ImmutableClazz.VALUE_PRIVATE) }.let { result -> assertEquals( TaskContract.Tasks.CLASSIFICATION_PRIVATE, @@ -243,7 +244,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Classification_Confidential() { buildTask { - classification = Clazz.CONFIDENTIAL + classification = Clazz(ImmutableClazz.VALUE_CONFIDENTIAL) }.let { result -> assertEquals( TaskContract.Tasks.CLASSIFICATION_CONFIDENTIAL, @@ -269,7 +270,7 @@ class DmfsTaskBuilderTest ( buildTask { }.let { result -> assertEquals( - TaskContract.Tasks.CLASSIFICATION_DEFAULT *//* null *//*, + TaskContract.Tasks.CLASSIFICATION_DEFAULT /* null */, result.getAsInteger(TaskContract.Tasks.CLASSIFICATION) ) } @@ -278,7 +279,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Status_NeedsAction() { buildTask { - status = Status.VTODO_NEEDS_ACTION + status = Status(ImmutableStatus.VALUE_NEEDS_ACTION) }.let { result -> assertEquals( TaskContract.Tasks.STATUS_NEEDS_ACTION, @@ -290,7 +291,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Status_Completed() { buildTask { - status = Status.VTODO_COMPLETED + status = Status(ImmutableStatus.VALUE_COMPLETED) }.let { result -> assertEquals( TaskContract.Tasks.STATUS_COMPLETED, @@ -302,7 +303,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Status_InProcess() { buildTask { - status = Status.VTODO_IN_PROCESS + status = Status(ImmutableStatus.VALUE_IN_PROCESS) }.let { result -> assertEquals( TaskContract.Tasks.STATUS_IN_PROCESS, @@ -314,7 +315,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Status_Cancelled() { buildTask { - status = Status.VTODO_CANCELLED + status = Status(ImmutableStatus.VALUE_CANCELLED) }.let { result -> assertEquals( TaskContract.Tasks.STATUS_CANCELLED, @@ -326,7 +327,13 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_DtStart() { buildTask { - dtStart = DtStart("20200703T155722", tzVienna) + dtStart = DtStart( + ZonedDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(15, 57, 22), + tzVienna.toZoneId() + ) + ) }.let { result -> Assert.assertEquals(1593784642000L, result.getAsLong(TaskContract.Tasks.DTSTART)) assertEquals(tzVienna.id, result.getAsString(TaskContract.Tasks.TZ)) @@ -337,7 +344,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_DtStart_AllDay() { buildTask { - dtStart = DtStart(Date("20200703")) + dtStart = DtStart(LocalDate.of(2020, 7, 3)) }.let { result -> Assert.assertEquals(1593734400000L, result.getAsLong(TaskContract.Tasks.DTSTART)) Assert.assertNull(result.get(TaskContract.Tasks.TZ)) @@ -348,7 +355,13 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Due() { buildTask { - due = Due(DateTime("20200703T155722", tzVienna)) + due = Due( + ZonedDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(15, 57, 22), + tzVienna.toZoneId() + ) + ) }.let { result -> Assert.assertEquals(1593784642000L, result.getAsLong(TaskContract.Tasks.DUE)) assertEquals(tzVienna.id, result.getAsString(TaskContract.Tasks.TZ)) @@ -359,7 +372,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Due_AllDay() { buildTask { - due = Due(Date("20200703")) + due = Due(LocalDate.of(2020, 7, 3)) }.let { result -> Assert.assertEquals(1593734400000L, result.getAsLong(TaskContract.Tasks.DUE)) Assert.assertNull(result.getAsString(TaskContract.Tasks.TZ)) @@ -370,8 +383,13 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_DtStart_NonAllDay_Due_AllDay() { buildTask { - dtStart = DtStart(DateTime("20200101T010203")) - due = Due(Date("20200201")) + dtStart = DtStart( + LocalDateTime.of( + LocalDate.of(2020, 1, 1), + LocalTime.of(1, 2, 3) + ) + ) + due = Due(LocalDate.of(2020, 2, 1)) }.let { result -> assertEquals( ZoneId.systemDefault().id, @@ -384,8 +402,13 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_DtStart_AllDay_Due_NonAllDay() { buildTask { - dtStart = DtStart(Date("20200101")) - due = Due(DateTime("20200201T010203")) + dtStart = DtStart(LocalDate.of(2020, 1, 1)) + due = Due( + LocalDateTime.of( + LocalDate.of(2020, 2, 1), + LocalTime.of(1, 2, 3) + ) + ) }.let { result -> Assert.assertNull(result.getAsString(TaskContract.Tasks.TZ)) assertEquals(1, result.getAsInteger(TaskContract.Tasks.IS_ALLDAY)) @@ -395,8 +418,8 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_DtStart_AllDay_Due_AllDay() { buildTask { - dtStart = DtStart(Date("20200101")) - due = Due(Date("20200201")) + dtStart = DtStart(LocalDate.of(2020, 1, 1)) + due = Due(LocalDate.of(2020, 2, 1)) }.let { result -> assertEquals(1, result.getAsInteger(TaskContract.Tasks.IS_ALLDAY)) } @@ -405,10 +428,19 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_DtStart_FloatingTime() { buildTask { - dtStart = DtStart("20200703T010203") + dtStart = DtStart( + LocalDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(1, 2, 3) + ) + ) }.let { result -> Assert.assertEquals( - DateTime("20200703T010203").time, + // we cannot hardcode the epoch timestamp since it depends on the system timezone (it's local) + LocalDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(1, 2, 3) + ).toEpochMilli(), result.getAsLong(TaskContract.Tasks.DTSTART) ) assertEquals( @@ -422,7 +454,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_DtStart_Utc() { buildTask { - dtStart = DtStart(DateTime(1593730923000), true) + dtStart = DtStart(Instant.ofEpochMilli(1593730923000)) }.let { result -> Assert.assertEquals(1593730923000L, result.getAsLong(TaskContract.Tasks.DTSTART)) assertEquals("Etc/UTC", result.getAsString(TaskContract.Tasks.TZ)) @@ -433,10 +465,18 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Due_FloatingTime() { buildTask { - due = Due("20200703T010203") + due = Due( + LocalDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(1, 2, 3) + ) + ) }.let { result -> Assert.assertEquals( - DateTime("20200703T010203").time, + LocalDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(1, 2, 3) + ).toEpochMilli(), result.getAsLong(TaskContract.Tasks.DUE) ) assertEquals( @@ -450,7 +490,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Due_Utc() { buildTask { - due = Due(DateTime(1593730923000).apply { isUtc = true }) + due = Due(Instant.ofEpochMilli(1593730923000)) }.let { result -> Assert.assertEquals(1593730923000L, result.getAsLong(TaskContract.Tasks.DUE)) assertEquals("Etc/UTC", result.getAsString(TaskContract.Tasks.TZ)) @@ -461,7 +501,7 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_Duration() { buildTask { - dtStart = DtStart(DateTime()) + dtStart = DtStart(Instant.now()) duration = Duration(null, "P1D") }.let { result -> assertEquals("P1D", result.get(TaskContract.Tasks.DURATION)) @@ -470,13 +510,13 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_CompletedAt() { - val now = DateTime() + val now = Instant.now() buildTask { completedAt = Completed(now) }.let { result -> // Note: iCalendar does not allow COMPLETED to be all-day [RFC 5545 3.8.2.1] assertEquals(0, result.getAsInteger(TaskContract.Tasks.COMPLETED_IS_ALLDAY)) - Assert.assertEquals(now.time, result.getAsLong(TaskContract.Tasks.COMPLETED)) + Assert.assertEquals(now.toEpochMilli(), result.getAsLong(TaskContract.Tasks.COMPLETED)) } } @@ -493,7 +533,7 @@ class DmfsTaskBuilderTest ( fun testBuildTask_RRule() { // Note: OpenTasks only supports one RRULE per VTODO (iCalendar: multiple RRULEs are allowed, but SHOULD not be used) buildTask { - rRule = RRule("FREQ=DAILY;COUNT=10") + rRule = RRule("FREQ=DAILY;COUNT=10") }.let { result -> assertEquals("FREQ=DAILY;COUNT=10", result.getAsString(TaskContract.Tasks.RRULE)) } @@ -502,11 +542,41 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_RDate() { buildTask { - dtStart = DtStart(DateTime("20200101T010203", tzVienna)) - rDates += RDate(DateList("20200102T020304", Value.DATE_TIME, tzVienna)) - rDates += RDate(DateList("20200102T020304", Value.DATE_TIME, tzChicago)) - rDates += RDate(DateList("20200103T020304Z", Value.DATE_TIME)) - rDates += RDate(DateList("20200103", Value.DATE)) + dtStart = DtStart( + ZonedDateTime.of( + LocalDate.of(2020, 1, 1), + LocalTime.of(1, 2, 3), + tzVienna.toZoneId() + ) + ) + rDates += RDate( + DateList( + ZonedDateTime.of( + LocalDate.of(2020, 1, 2), + LocalTime.of(2, 3, 4), + tzVienna.toZoneId() + ) + ) + ) + rDates += RDate( + DateList( + ZonedDateTime.of( + LocalDate.of(2020, 1, 2), + LocalTime.of(2, 3, 4), + tzChicago.toZoneId() + ) + ) + ) + rDates += RDate( + DateList( + ZonedDateTime.of( + LocalDate.of(2020, 1, 3), + LocalTime.of(2, 3, 4), + ZoneOffset.UTC + ) + ) + ) + rDates += RDate(DateList(LocalDate.of(2020, 1, 3))) }.let { result -> assertEquals(tzVienna.id, result.getAsString(TaskContract.Tasks.TZ)) assertEquals( @@ -519,12 +589,34 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_ExDate() { buildTask { - dtStart = DtStart(DateTime("20200101T010203", tzVienna)) - rRule = RRule("FREQ=DAILY;COUNT=10") - exDates += ExDate(DateList("20200102T020304", Value.DATE_TIME, tzVienna)) - exDates += ExDate(DateList("20200102T020304", Value.DATE_TIME, tzChicago)) - exDates += ExDate(DateList("20200103T020304Z", Value.DATE_TIME)) - exDates += ExDate(DateList("20200103", Value.DATE)) + dtStart = DtStart(ZonedDateTime.of( + LocalDate.of(2020, 1, 2), + LocalTime.of(1, 2, 3), + tzVienna.toZoneId() + )) + rRule = RRule("FREQ=DAILY;COUNT=10") + exDates += ExDate(DateList( + ZonedDateTime.of( + LocalDate.of(2020, 1, 2), + LocalTime.of(2, 3, 4), + tzVienna.toZoneId() + ) + )) + exDates += ExDate(DateList( + ZonedDateTime.of( + LocalDate.of(2020, 1, 2), + LocalTime.of(2, 3, 4), + tzChicago.toZoneId() + ) + )) + exDates += ExDate(DateList( + ZonedDateTime.of( + LocalDate.of(2020, 1, 3), + LocalTime.of(2, 3, 4), + ZoneOffset.UTC + ) + )) + exDates += ExDate(DateList(LocalDate.of(2020, 1, 3))) }.let { result -> assertEquals(tzVienna.id, result.getAsString(TaskContract.Tasks.TZ)) assertEquals( @@ -606,7 +698,7 @@ class DmfsTaskBuilderTest ( fun testBuildTask_RelatedTo_Parent() { buildTask { relatedTo.add(RelatedTo("Parent-Task").apply { - parameters.add(RelType.PARENT) + add(RelType.PARENT) }) }.let { result -> val taskId = result.getAsLong(TaskContract.Tasks._ID) @@ -627,7 +719,7 @@ class DmfsTaskBuilderTest ( fun testBuildTask_RelatedTo_Child() { buildTask { relatedTo.add(RelatedTo("Child-Task").apply { - parameters.add(RelType.CHILD) + add(RelType.CHILD) }) }.let { result -> val taskId = result.getAsLong(TaskContract.Tasks._ID) @@ -648,7 +740,7 @@ class DmfsTaskBuilderTest ( fun testBuildTask_RelatedTo_Sibling() { buildTask { relatedTo.add(RelatedTo("Sibling-Task").apply { - parameters.add(RelType.SIBLING) + add(RelType.SIBLING) }) }.let { result -> val taskId = result.getAsLong(TaskContract.Tasks._ID) @@ -669,7 +761,7 @@ class DmfsTaskBuilderTest ( fun testBuildTask_RelatedTo_Custom() { buildTask { relatedTo.add(RelatedTo("Sibling-Task").apply { - parameters.add(RelType("custom-relationship")) + add(RelType("custom-relationship")) }) }.let { result -> val taskId = result.getAsLong(TaskContract.Tasks._ID) @@ -709,8 +801,8 @@ class DmfsTaskBuilderTest ( @Test fun testBuildTask_UnknownProperty() { val xProperty = XProperty("X-TEST-PROPERTY", "test-value").apply { - parameters.add(TzId(tzVienna.id)) - parameters.add(XParameter("X-TEST-PARAMETER", "12345")) + add(TzId(tzVienna.id)) + add(XParameter("X-TEST-PARAMETER", "12345")) } buildTask { unknownProperties.add(xProperty) @@ -731,8 +823,8 @@ class DmfsTaskBuilderTest ( task.summary = "All-day task" task.description = "All-day task for testing" task.location = "Sample location testBuildAllDayTask" - task.dtStart = DtStart(Date("20150501")) - task.due = Due(Date("20150502")) + task.dtStart = DtStart(LocalDate.of(2015, 5, 1)) + task.due = Due(LocalDate.of(2015, 5, 2)) Assert.assertTrue(task.isAllDay()) val uri = DmfsTask(taskList!!, task, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0).add() Assert.assertNotNull(uri) @@ -766,7 +858,7 @@ class DmfsTaskBuilderTest ( val task = Task() val builder = DmfsTaskBuilder(taskList!!, task, 0, "410c19d7-df79-4d65-8146-40b7bec5923b", null, 0) val dmfsTask = DmfsTask(taskList!!, task, "410c19d7-df79-4d65-8146-40b7bec5923b", null, 0) - dmfsTask.task!!.dtStart = DtStart("20150101") + dmfsTask.task!!.dtStart = DtStart(LocalDate.of(2015, 1, 1)) assertEquals(tzDefault, builder.getTimeZone()) } @@ -775,9 +867,9 @@ class DmfsTaskBuilderTest ( val task = Task() val builder = DmfsTaskBuilder(taskList!!, task, 0, "9468a4cf-0d5b-4379-a704-12f1f84100ba", null, 0) val dmfsTask = DmfsTask(taskList!!, task, "9dc64544-1816-4f04-b952-e894164467f6", null, 0) - dmfsTask.task!!.dtStart = DtStart("20150101", tzVienna) + dmfsTask.task!!.dtStart = DtStart(LocalDate.of(2015, 1, 1).atStartOfDay(tzVienna.toZoneId())) assertEquals(tzVienna, builder.getTimeZone()) - }*/ + } // helpers diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessorTest.kt new file mode 100644 index 00000000..656ac9e0 --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessorTest.kt @@ -0,0 +1,337 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.tasks + +import android.accounts.Account +import android.content.ContentUris +import android.content.ContentValues +import android.net.Uri +import at.bitfire.ical4android.DmfsStyleProvidersTaskTest +import at.bitfire.ical4android.Task +import at.bitfire.ical4android.TaskProvider +import at.bitfire.ical4android.impl.TestTaskList +import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration +import at.bitfire.synctools.storage.tasks.DmfsTaskList +import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.property.immutable.ImmutableClazz +import net.fortuna.ical4j.model.property.immutable.ImmutableStatus +import org.dmfs.tasks.contract.TaskContract +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.time.Instant +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime + +class DmfsTaskProcessorTest( + providerName: TaskProvider.ProviderName +) : DmfsStyleProvidersTaskTest(providerName) { + + private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()!! + private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna")!! + private val tzDefault = tzRegistry.getTimeZone(ZoneId.systemDefault().id)!! + + private val testAccount = Account(javaClass.name, TaskContract.LOCAL_ACCOUNT_TYPE) + + private lateinit var taskListUri: Uri + private var taskList: DmfsTaskList? = null + private lateinit var processor: DmfsTaskProcessor + + @Before + override fun prepare() { + super.prepare() + + taskList = TestTaskList.create(testAccount, provider) + assertNotNull("Couldn't find/create test task list", taskList) + + taskListUri = ContentUris.withAppendedId(provider.taskListsUri(), taskList!!.id) + processor = DmfsTaskProcessor(taskList!!) + } + + @After + override fun shutdown() { + taskList?.delete() + super.shutdown() + } + + // populateTask tests + + @Test + fun testPopulateTask_BasicProperties() { + val values = ContentValues().apply { + put(TaskContract.Tasks._UID, "test-uid-123") + put(TaskContract.Tasks.SYNC_VERSION, 5) + put(TaskContract.Tasks.TITLE, "Test Task") + put(TaskContract.Tasks.LOCATION, "Test Location") + put(TaskContract.Tasks.DESCRIPTION, "Test Description") + put(TaskContract.Tasks.URL, "https://example.com") + put(TaskContract.Tasks.TASK_COLOR, 0x123456) + put(TaskContract.Tasks.PRIORITY, 3) + } + + val task = Task() + processor.populateTask(values, task) + + assertEquals("test-uid-123", task.uid) + assertEquals(5, task.sequence) + assertEquals("Test Task", task.summary) + assertEquals("Test Location", task.location) + assertEquals("Test Description", task.description) + assertEquals("https://example.com", task.url) + assertEquals(0x123456, task.color) + assertEquals(3, task.priority) + } + + @Test + fun testPopulateTask_GeoPosition() { + val values = ContentValues().apply { + put(TaskContract.Tasks.GEO, "16.159601,47.913563") + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.geoPosition) + assertEquals(47.913563.toBigDecimal(), task.geoPosition!!.latitude) + assertEquals(16.159601.toBigDecimal(), task.geoPosition!!.longitude) + } + + @Test + fun testPopulateTask_GeoPosition_Invalid() { + val values = ContentValues().apply { + put(TaskContract.Tasks.GEO, "invalid-geo-data") + } + + val task = Task() + processor.populateTask(values, task) + + assertNull(task.geoPosition) + } + + @Test + fun testPopulateTask_Organizer() { + val values = ContentValues().apply { + put(TaskContract.Tasks.ORGANIZER, "organizer@example.com") + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.organizer) + assertEquals("mailto:organizer@example.com", task.organizer!!.value) + } + + @Test + fun testPopulateTask_Classification() { + // Test PUBLIC + var values = ContentValues().apply { + put(TaskContract.Tasks.CLASSIFICATION, TaskContract.Tasks.CLASSIFICATION_PUBLIC) + } + var task = Task() + processor.populateTask(values, task) + assertNotNull(task.classification) + assertEquals(ImmutableClazz.VALUE_PUBLIC, task.classification!!.value) + + // Test PRIVATE + values = ContentValues().apply { + put(TaskContract.Tasks.CLASSIFICATION, TaskContract.Tasks.CLASSIFICATION_PRIVATE) + } + task = Task() + processor.populateTask(values, task) + assertNotNull(task.classification) + assertEquals(ImmutableClazz.VALUE_PRIVATE, task.classification!!.value) + + // Test CONFIDENTIAL + values = ContentValues().apply { + put(TaskContract.Tasks.CLASSIFICATION, TaskContract.Tasks.CLASSIFICATION_CONFIDENTIAL) + } + task = Task() + processor.populateTask(values, task) + assertNotNull(task.classification) + assertEquals(ImmutableClazz.VALUE_CONFIDENTIAL, task.classification!!.value) + + // Test default (unknown) + values = ContentValues().apply { + put(TaskContract.Tasks.CLASSIFICATION, 999) + } + task = Task() + processor.populateTask(values, task) + assertNull(task.classification) + } + + @Test + fun testPopulateTask_Status() { + // Test NEEDS_ACTION + var values = ContentValues().apply { + put(TaskContract.Tasks.STATUS, TaskContract.Tasks.STATUS_NEEDS_ACTION) + } + var task = Task() + processor.populateTask(values, task) + assertNotNull(task.status) + assertEquals(ImmutableStatus.VALUE_NEEDS_ACTION, task.status!!.value) + + // Test COMPLETED + values = ContentValues().apply { + put(TaskContract.Tasks.STATUS, TaskContract.Tasks.STATUS_COMPLETED) + } + task = Task() + processor.populateTask(values, task) + assertNotNull(task.status) + assertEquals(ImmutableStatus.VALUE_COMPLETED, task.status!!.value) + + // Test IN_PROCESS + values = ContentValues().apply { + put(TaskContract.Tasks.STATUS, TaskContract.Tasks.STATUS_IN_PROCESS) + } + task = Task() + processor.populateTask(values, task) + assertNotNull(task.status) + assertEquals(ImmutableStatus.VALUE_IN_PROCESS, task.status!!.value) + + // Test CANCELLED + values = ContentValues().apply { + put(TaskContract.Tasks.STATUS, TaskContract.Tasks.STATUS_CANCELLED) + } + task = Task() + processor.populateTask(values, task) + assertNotNull(task.status) + assertEquals(ImmutableStatus.VALUE_CANCELLED, task.status!!.value) + + // Test default + values = ContentValues() + task = Task() + processor.populateTask(values, task) + assertNotNull(task.status) + assertEquals(ImmutableStatus.VALUE_NEEDS_ACTION, task.status!!.value) + } + + @Test + fun testPopulateTask_CompletedAndPercentComplete() { + val now = Instant.now() + val values = ContentValues().apply { + put(TaskContract.Tasks.COMPLETED, now.toEpochMilli()) + put(TaskContract.Tasks.PERCENT_COMPLETE, 75) + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.completedAt) + assertEquals(now.toEpochMilli(), task.completedAt!!.date.toEpochMilli()) + assertEquals(75, task.percentComplete) + } + + @Test + fun testPopulateTask_DtStart_AllDay() { + val date = LocalDate.of(2020, 7, 3) + val values = ContentValues().apply { + put(TaskContract.Tasks.DTSTART, date.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()) + put(TaskContract.Tasks.IS_ALLDAY, 1) + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.dtStart) + assertTrue(task.dtStart!!.date is LocalDate) + assertEquals(date, task.dtStart!!.date as LocalDate) + } + + @Test + fun testPopulateTask_DtStart_WithTimezone() { + val instant = ZonedDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(15, 57, 22), + tzVienna.toZoneId() + ).toInstant() + + val values = ContentValues().apply { + put(TaskContract.Tasks.DTSTART, instant.toEpochMilli()) + put(TaskContract.Tasks.TZ, tzVienna.id) + put(TaskContract.Tasks.IS_ALLDAY, 0) + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.dtStart) + assertTrue(task.dtStart!!.date is ZonedDateTime) + assertEquals(instant, (task.dtStart!!.date as ZonedDateTime).toInstant()) + } + + @Test + fun testPopulateTask_Due_AllDay() { + val date = LocalDate.of(2020, 7, 3) + val values = ContentValues().apply { + put(TaskContract.Tasks.DUE, date.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()) + put(TaskContract.Tasks.IS_ALLDAY, 1) + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.due) + assertTrue(task.due!!.date is LocalDate) + assertEquals(date, task.due!!.date as LocalDate) + } + + @Test + fun testPopulateTask_Due_WithTimezone() { + val instant = ZonedDateTime.of( + LocalDate.of(2020, 7, 3), + LocalTime.of(15, 57, 22), + tzVienna.toZoneId() + ).toInstant() + + val values = ContentValues().apply { + put(TaskContract.Tasks.DUE, instant.toEpochMilli()) + put(TaskContract.Tasks.TZ, tzVienna.id) + put(TaskContract.Tasks.IS_ALLDAY, 0) + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.due) + assertTrue(task.due!!.date is ZonedDateTime) + assertEquals(instant, (task.due!!.date as ZonedDateTime).toInstant()) + } + + @Test + fun testPopulateTask_Duration() { + val values = ContentValues().apply { + put(TaskContract.Tasks.DURATION, "P1DT2H30M") + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.duration) + assertEquals("P1DT2H30M", task.duration!!.duration.toRfc5545Duration(Instant.now())) + } + + @Test + fun testPopulateTask_RRule() { + val values = ContentValues().apply { + put(TaskContract.Tasks.RRULE, "FREQ=DAILY;COUNT=10") + } + + val task = Task() + processor.populateTask(values, task) + + assertNotNull(task.rRule) + assertEquals("FREQ=DAILY;COUNT=10", task.rRule!!.value) + } + +} diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt index 764625a7..3cfc9db0 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt @@ -6,11 +6,15 @@ package at.bitfire.ical4android +import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory +import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistry import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.TimeZoneRegistryImpl +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.property.TzId import java.time.ZoneId import java.util.logging.Logger @@ -50,8 +54,12 @@ class AndroidCompatTimeZoneRegistry( // If the timezone is empty, or format is not valid, return null if (id.isEmpty()) return null - val tz: TimeZone = base.getTimeZone(id) - ?: return null // ical4j doesn't know time zone, return null + val tz: TimeZone = try { + base.getTimeZone(id) ?: return null // ical4j doesn't know time zone, return null + } catch (_: NullPointerException) { + // The registry sometimes throws NullPointerException instead of returning null + return null + } // check whether time zone is available on Android, too val androidTzId = @@ -76,14 +84,12 @@ class AndroidCompatTimeZoneRegistry( logger.fine("Using ical4j timezone ${tz.id} data to construct Android timezone $androidTzId") // create a copy of the VTIMEZONE so that we don't modify the original registry values (which are not immutable) - val vTimeZone = tz.vTimeZone - TODO("ical4j 4.x") - /*val newVTimeZoneProperties = PropertyList() - newVTimeZoneProperties += TzId(androidTzId) - return TimeZone(VTimeZone( - newVTimeZoneProperties, - vTimeZone.observances - ))*/ + return TimeZone( + VTimeZone( + PropertyList(listOf(TzId(androidTzId))), + ComponentList(tz.vTimeZone.observances) + ) + ) } else return tz } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt index 900c51a3..621bb1cf 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/Task.kt @@ -7,6 +7,7 @@ package at.bitfire.ical4android import androidx.annotation.IntRange +import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.property.Clazz @@ -69,10 +70,9 @@ data class Task( ) : ICalendar() { fun isAllDay(): Boolean { - TODO("ical4j 4.x") - /*return dtStart?.let { DateUtils.isDate(it) } + return dtStart?.let { DateUtils.isDate(it) } ?: due?.let { DateUtils.isDate(it) } - ?: true*/ + ?: true } } diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index 226892d2..2d9dfaf1 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -11,7 +11,17 @@ import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.property.DateProperty import java.io.StringReader +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.temporal.ChronoField +import java.time.temporal.ChronoUnit +import java.time.temporal.Temporal +import java.util.logging.Logger /** * Date/time utilities @@ -53,8 +63,7 @@ object DateUtils { * @return *true* if the date is a DATE-TIME value; *false* otherwise (for instance, when the argument is a DATE value or null) */ fun isDateTime(date: DateProperty<*>?): Boolean = - TODO("ical4j 4.x") - //date != null && date.date is DateTime + date != null && TemporalAdapter.isDateTimePrecision(date.date) /** * Parses an iCalendar that only contains a `VTIMEZONE` definition to a VTimeZone object. @@ -75,4 +84,39 @@ object DateUtils { } } + /** + * Converts the given [Instant] by truncating it to days, and converting into [LocalDate] by its + * epoch timestamp. + */ + fun Instant.toLocalDate(): LocalDate { + val epochSeconds = truncatedTo(ChronoUnit.DAYS).epochSecond + return LocalDate.ofEpochDay(epochSeconds / (24 * 60 * 60 /*seconds in a day*/)) + } + + /** + * Converts the given generic [Temporal] into milliseconds since epoch. + * @param fallbackTimezone Any specific timezone to use as fallback if there's not enough + * information on the [Temporal] type (local types). Defaults to UTC. + * @throws IllegalArgumentException if the [Temporal] is from an unknown time, which also doesn't + * support [ChronoField.INSTANT_SECONDS] + */ + fun Temporal.toEpochMilli(fallbackTimezone: ZoneId? = null): Long { + // If the temporal supports instant seconds, we can compute epoch millis directly from them + if (isSupported(ChronoField.INSTANT_SECONDS)) { + val seconds = getLong(ChronoField.INSTANT_SECONDS) + val nanos = get(ChronoField.NANO_OF_SECOND) + // Convert seconds and nanos to millis + return (seconds * 1000) + (nanos / 1_000_000) + } + + return when (this) { + is Instant -> this.toEpochMilli() + is ZonedDateTime -> this.toInstant().toEpochMilli() + is OffsetDateTime -> this.toInstant().toEpochMilli() + is LocalDate -> this.atStartOfDay(fallbackTimezone ?: ZoneOffset.UTC).toInstant().toEpochMilli() + is LocalDateTime -> this.atZone(fallbackTimezone ?: ZoneOffset.UTC).toInstant().toEpochMilli() + else -> throw IllegalArgumentException("${this::class.java.simpleName} cannot be converted to epoch millis.") + } + } + } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt index a7ff6096..b680b4b2 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskBuilder.kt @@ -12,19 +12,26 @@ import at.bitfire.ical4android.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA import at.bitfire.ical4android.ICalendar import at.bitfire.ical4android.Task import at.bitfire.ical4android.UnknownProperty +import at.bitfire.ical4android.util.DateUtils.toEpochMilli +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.storage.BatchOperation.CpoBuilder import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.synctools.storage.tasks.TasksBatchOperation import at.bitfire.synctools.util.AndroidTimeUtils import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.Email import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.parameter.TzId import net.fortuna.ical4j.model.property.Action -import net.fortuna.ical4j.model.property.Clazz -import net.fortuna.ical4j.model.property.Status +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due +import net.fortuna.ical4j.model.property.immutable.ImmutableAction +import net.fortuna.ical4j.model.property.immutable.ImmutableClazz +import net.fortuna.ical4j.model.property.immutable.ImmutableStatus import net.fortuna.ical4j.util.TimeZones import org.dmfs.tasks.contract.TaskContract.Properties import org.dmfs.tasks.contract.TaskContract.Property.Alarm @@ -36,6 +43,7 @@ import java.time.ZoneId import java.util.Locale import java.util.logging.Level import java.util.logging.Logger +import kotlin.jvm.optionals.getOrNull /** * Writes [at.bitfire.ical4android.Task] to dmfs task provider data rows @@ -94,15 +102,14 @@ class DmfsTaskBuilder( .withValue(Tasks.PARENT_ID, null) // organizer - TODO("ical4j 4.x") // Note: big method – maybe split? Depends on how we want to proceed with refactoring. - /*task.organizer?.let { organizer -> + task.organizer?.let { organizer -> val uri = organizer.calAddress val email = if (uri.scheme.equals("mailto", true)) uri.schemeSpecificPart else - organizer.getParameter(Parameter.EMAIL)?.value + organizer.getParameter(Parameter.EMAIL).getOrNull()?.value if (email != null) builder.withValue(Tasks.ORGANIZER, email) else @@ -110,25 +117,27 @@ class DmfsTaskBuilder( } // Priority, classification - builder .withValue(Tasks.PRIORITY, task.priority) - .withValue(Tasks.CLASSIFICATION, when (task.classification) { - Clazz.PUBLIC -> Tasks.CLASSIFICATION_PUBLIC - Clazz.CONFIDENTIAL -> Tasks.CLASSIFICATION_CONFIDENTIAL + builder + .withValue(Tasks.PRIORITY, task.priority) + .withValue(Tasks.CLASSIFICATION, when (task.classification?.value?.uppercase()) { + ImmutableClazz.VALUE_PUBLIC -> Tasks.CLASSIFICATION_PUBLIC + ImmutableClazz.VALUE_CONFIDENTIAL -> Tasks.CLASSIFICATION_CONFIDENTIAL null -> Tasks.CLASSIFICATION_DEFAULT else -> Tasks.CLASSIFICATION_PRIVATE // all unknown classifications MUST be treated as PRIVATE }) // COMPLETED must always be a DATE-TIME - builder .withValue(Tasks.COMPLETED, task.completedAt?.date?.time) + builder + .withValue(Tasks.COMPLETED, task.completedAt?.date?.toEpochMilli()) .withValue(Tasks.COMPLETED_IS_ALLDAY, 0) .withValue(Tasks.PERCENT_COMPLETE, task.percentComplete) // Status - val status = when (task.status) { - Status.VTODO_IN_PROCESS -> Tasks.STATUS_IN_PROCESS - Status.VTODO_COMPLETED -> Tasks.STATUS_COMPLETED - Status.VTODO_CANCELLED -> Tasks.STATUS_CANCELLED - else -> Tasks.STATUS_DEFAULT // == Tasks.STATUS_NEEDS_ACTION + val status = when (task.status?.value) { + ImmutableStatus.VALUE_IN_PROCESS -> Tasks.STATUS_IN_PROCESS + ImmutableStatus.VALUE_COMPLETED -> Tasks.STATUS_COMPLETED + ImmutableStatus.VALUE_CANCELLED -> Tasks.STATUS_CANCELLED + else -> Tasks.STATUS_DEFAULT // == Tasks.STATUS_NEEDS_ACTION } builder.withValue(Tasks.STATUS, status) @@ -138,16 +147,17 @@ class DmfsTaskBuilder( builder .withValue(Tasks.IS_ALLDAY, 1) .withValue(Tasks.TZ, null) } else { - AndroidTimeUtils.androidifyTimeZone(task.dtStart, tzRegistry) - AndroidTimeUtils.androidifyTimeZone(task.due, tzRegistry) + task.dtStart = task.dtStart?.normalizedDate()?.let { DtStart(it) } + task.due = task.due?.normalizedDate()?.let { Due(it) } builder .withValue(Tasks.IS_ALLDAY, 0) .withValue(Tasks.TZ, getTimeZone().id) } - builder .withValue(Tasks.CREATED, task.createdAt) + builder + .withValue(Tasks.CREATED, task.createdAt) .withValue(Tasks.LAST_MODIFIED, task.lastModified) - .withValue(Tasks.DTSTART, task.dtStart?.date?.time) - .withValue(Tasks.DUE, task.due?.date?.time) + .withValue(Tasks.DTSTART, task.dtStart?.date?.toEpochMilli()) + .withValue(Tasks.DUE, task.due?.date?.toEpochMilli()) .withValue(Tasks.DURATION, task.duration?.value) .withValue(Tasks.RDATE, @@ -163,24 +173,29 @@ class DmfsTaskBuilder( else AndroidTimeUtils.recurrenceSetsToOpenTasksString(task.exDates, if (allDay) null else getTimeZone())) - logger.log(Level.FINE, "Built task object", builder.build())*/ + logger.log(Level.FINE, "Built task object", builder.build()) } fun getTimeZone(): TimeZone { - TODO("ical4j 4.x") - /*return task.dtStart?.let { dtStart -> + var tzId = task.dtStart?.let { dtStart -> if (dtStart.isUtc) - tzRegistry.getTimeZone(TimeZones.UTC_ID) + TimeZones.UTC_ID else - dtStart.timeZone + dtStart.getParameter(Parameter.TZID).getOrNull()?.value } ?: task.due?.let { due -> if (due.isUtc) - tzRegistry.getTimeZone(TimeZones.UTC_ID) + TimeZones.UTC_ID else - due.timeZone + due.getParameter(Parameter.TZID).getOrNull()?.value } ?: - tzRegistry.getTimeZone(ZoneId.systemDefault().id)!!*/ + ZoneId.systemDefault().id + + // 'Z' is not a valid timezone id, replace it by the UTC definition + if (tzId == "Z") tzId = TimeZones.UTC_ID + + val timeZone: TimeZone? = tzRegistry.getTimeZone(tzId) + return timeZone ?: throw NullPointerException("Could not find timezone '$tzId' in registry.") } fun insertProperties(batch: TasksBatchOperation, idxTask: Int?) { @@ -207,16 +222,13 @@ class DmfsTaskBuilder( Alarm.ALARM_REFERENCE_START_DATE } - TODO("ical4j 4.x") - /*val alarmType = when (alarm.action?.value?.uppercase(Locale.ROOT)) { - Action.AUDIO.value -> - Alarm.ALARM_TYPE_SOUND - Action.DISPLAY.value -> - Alarm.ALARM_TYPE_MESSAGE - Action.EMAIL.value -> - Alarm.ALARM_TYPE_EMAIL - else -> - Alarm.ALARM_TYPE_NOTHING + val alarmType = when ( + alarm.getProperty(Property.ACTION).getOrNull()?.value?.uppercase(Locale.ROOT) + ) { + ImmutableAction.VALUE_AUDIO -> Alarm.ALARM_TYPE_SOUND + ImmutableAction.VALUE_DISPLAY -> Alarm.ALARM_TYPE_MESSAGE + ImmutableAction.VALUE_EMAIL -> Alarm.ALARM_TYPE_EMAIL + else -> Alarm.ALARM_TYPE_NOTHING } val builder = CpoBuilder @@ -229,7 +241,7 @@ class DmfsTaskBuilder( .withValue(Alarm.ALARM_TYPE, alarmType) logger.log(Level.FINE, "Inserting alarm", builder.build()) - batch += builder*/ + batch += builder } } @@ -255,15 +267,11 @@ class DmfsTaskBuilder( } private fun insertRelatedTo(batch: TasksBatchOperation, idxTask: Int?) { - TODO("ical4j 4.x") - /*for (relatedTo in task.relatedTo) { - val relType = when ((relatedTo.getParameter(Parameter.RELTYPE) as RelType?)) { - RelType.CHILD -> - Relation.RELTYPE_CHILD - RelType.SIBLING -> - Relation.RELTYPE_SIBLING - else *//* RelType.PARENT, default value *//* -> - Relation.RELTYPE_PARENT + for (relatedTo in task.relatedTo) { + val relType = when ((relatedTo.getParameter(Parameter.RELTYPE)).getOrNull()) { + RelType.CHILD -> Relation.RELTYPE_CHILD + RelType.SIBLING -> Relation.RELTYPE_SIBLING + else /* RelType.PARENT, default value */ -> Relation.RELTYPE_PARENT } val builder = CpoBuilder.newInsert(taskList.tasksPropertiesUri()) .withTaskId(Relation.TASK_ID, idxTask) @@ -272,7 +280,7 @@ class DmfsTaskBuilder( .withValue(Relation.RELATED_TYPE, relType) logger.log(Level.FINE, "Inserting relation", builder.build()) batch += builder - }*/ + } } private fun insertUnknownProperties(batch: TasksBatchOperation, idxTask: Int?) { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt index b61f95d3..87a8c6f6 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/tasks/DmfsTaskProcessor.kt @@ -10,12 +10,10 @@ import android.content.ContentValues import at.bitfire.ical4android.DmfsTask.Companion.UNKNOWN_PROPERTY_DATA import at.bitfire.ical4android.Task import at.bitfire.ical4android.UnknownProperty +import at.bitfire.ical4android.util.DateUtils.toLocalDate +import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.storage.tasks.DmfsTaskList import at.bitfire.synctools.util.AndroidTimeUtils -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.RelType @@ -42,6 +40,8 @@ import org.dmfs.tasks.contract.TaskContract.Property.Comment import org.dmfs.tasks.contract.TaskContract.Property.Relation import org.dmfs.tasks.contract.TaskContract.Tasks import java.net.URISyntaxException +import java.time.Instant +import java.time.temporal.Temporal import java.util.logging.Level import java.util.logging.Logger @@ -65,7 +65,7 @@ class DmfsTaskProcessor( to.location = values.getAsString(Tasks.LOCATION) to.userAgents += taskList.providerName.packageName - values.getAsString(Tasks.GEO)?.let { geo -> + values.getAsString(Tasks.GEO)?.takeIf { it.contains(",") }?.let { geo -> val (lng, lat) = geo.split(',') try { to.geoPosition = Geo(lat.toBigDecimal(), lng.toBigDecimal()) @@ -88,24 +88,23 @@ class DmfsTaskProcessor( values.getAsInteger(Tasks.PRIORITY)?.let { to.priority = it } - TODO("ical4j 4.x") // Note: big method – maybe split? Depends on how we want to proceed with refactoring. - /*to.classification = when (values.getAsInteger(Tasks.CLASSIFICATION)) { - Tasks.CLASSIFICATION_PUBLIC -> Clazz.PUBLIC - Tasks.CLASSIFICATION_PRIVATE -> Clazz.PRIVATE - Tasks.CLASSIFICATION_CONFIDENTIAL -> Clazz.CONFIDENTIAL + to.classification = when (values.getAsInteger(Tasks.CLASSIFICATION)) { + Tasks.CLASSIFICATION_PUBLIC -> Clazz(Clazz.VALUE_PUBLIC) + Tasks.CLASSIFICATION_PRIVATE -> Clazz(Clazz.VALUE_PRIVATE) + Tasks.CLASSIFICATION_CONFIDENTIAL -> Clazz(Clazz.VALUE_CONFIDENTIAL) else -> null } - values.getAsLong(Tasks.COMPLETED)?.let { to.completedAt = Completed(DateTime(it)) } + values.getAsLong(Tasks.COMPLETED)?.let { to.completedAt = Completed(Instant.ofEpochMilli(it)) } values.getAsInteger(Tasks.PERCENT_COMPLETE)?.let { to.percentComplete = it } to.status = when (values.getAsInteger(Tasks.STATUS)) { - Tasks.STATUS_IN_PROCESS -> Status.VTODO_IN_PROCESS - Tasks.STATUS_COMPLETED -> Status.VTODO_COMPLETED - Tasks.STATUS_CANCELLED -> Status.VTODO_CANCELLED - else -> Status.VTODO_NEEDS_ACTION + Tasks.STATUS_IN_PROCESS -> Status(Status.VALUE_IN_PROCESS) + Tasks.STATUS_COMPLETED -> Status(Status.VALUE_COMPLETED) + Tasks.STATUS_CANCELLED -> Status(Status.VALUE_CANCELLED) + else -> Status(Status.VALUE_NEEDS_ACTION) } val allDay = (values.getAsInteger(Tasks.IS_ALLDAY) ?: 0) != 0 @@ -120,34 +119,28 @@ class DmfsTaskProcessor( values.getAsLong(Tasks.LAST_MODIFIED)?.let { to.lastModified = it } values.getAsLong(Tasks.DTSTART)?.let { dtStart -> + val instant = Instant.ofEpochMilli(dtStart) to.dtStart = if (allDay) - DtStart(Date(dtStart)) + DtStart(instant.toLocalDate()) else { - val dt = DateTime(dtStart) if (tz == null) - DtStart(dt, true) + DtStart(instant) else - DtStart(dt.apply { - timeZone = tz - }) + DtStart(instant.atZone(tz.toZoneId())) } } values.getAsLong(Tasks.DUE)?.let { due -> + val instant = Instant.ofEpochMilli(due) to.due = if (allDay) - Due(Date(due)) + Due(instant.toLocalDate()) else { - val dt = DateTime(due) if (tz == null) - Due(dt).apply { - isUtc = true - } + Due(instant) else - Due(dt.apply { - timeZone = tz - }) + Due(instant.atZone(tz.toZoneId())) } } @@ -163,7 +156,7 @@ class DmfsTaskProcessor( to.exDates += AndroidTimeUtils.androidStringToRecurrenceSet(it, tzRegistry, allDay) { dates -> ExDate(dates) } } - values.getAsString(Tasks.RRULE)?.let { to.rRule = RRule(it) }*/ + values.getAsString(Tasks.RRULE)?.let { to.rRule = RRule(it) } } fun populateProperty(row: ContentValues, to: Task) { @@ -186,29 +179,24 @@ class DmfsTaskProcessor( } private fun populateAlarm(row: ContentValues, to: Task) { - val props = PropertyList() - - val trigger = Trigger(java.time.Duration.ofMinutes(-row.getAsLong(Alarm.MINUTES_BEFORE))) - TODO("ical4j 4.x") - /*when (row.getAsInteger(Alarm.REFERENCE)) { - Alarm.ALARM_REFERENCE_START_DATE -> - trigger.parameters.add(Related.START) - Alarm.ALARM_REFERENCE_DUE_DATE -> - trigger.parameters.add(Related.END) - } - props += trigger - - props += when (row.getAsInteger(Alarm.ALARM_TYPE)) { - Alarm.ALARM_TYPE_EMAIL -> - Action.EMAIL - Alarm.ALARM_TYPE_SOUND -> - Action.AUDIO - else -> - // show alarm by default - Action.DISPLAY - } - - props += Description(row.getAsString(Alarm.MESSAGE) ?: to.summary)*/ + val props = propertyListOf( + Trigger(java.time.Duration.ofMinutes(-row.getAsLong(Alarm.MINUTES_BEFORE))).let { + when (row.getAsInteger(Alarm.REFERENCE)) { + Alarm.ALARM_REFERENCE_START_DATE -> it.add(Related.START) + Alarm.ALARM_REFERENCE_DUE_DATE -> it.add(Related.END) + else -> it + } + }, + Action( + when (row.getAsInteger(Alarm.ALARM_TYPE)) { + Alarm.ALARM_TYPE_EMAIL -> Action.VALUE_EMAIL + Alarm.ALARM_TYPE_SOUND -> Action.VALUE_AUDIO + // show alarm by default + else -> Action.VALUE_DISPLAY + } + ), + Description(row.getAsString(Alarm.MESSAGE) ?: to.summary) + ) to.alarms += VAlarm(props) } @@ -220,20 +208,20 @@ class DmfsTaskProcessor( return } - val relatedTo = RelatedTo(uid) - - // add relation type as reltypeparam - TODO("ical4j 4.x") - /*relatedTo.parameters.add(when (row.getAsInteger(Relation.RELATED_TYPE)) { - Relation.RELTYPE_CHILD -> - RelType.CHILD - Relation.RELTYPE_SIBLING -> - RelType.SIBLING - else *//* Relation.RELTYPE_PARENT, default value *//* -> - RelType.PARENT - })*/ - - to.relatedTo.add(relatedTo) + to.relatedTo.add( + RelatedTo(uid) + // add relation type as reltypeparam + .add( + when (row.getAsInteger(Relation.RELATED_TYPE)) { + Relation.RELTYPE_CHILD -> + RelType.CHILD + Relation.RELTYPE_SIBLING -> + RelType.SIBLING + else /* Relation.RELTYPE_PARENT, default value */ -> + RelType.PARENT + } + ) + ) } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt index 56829fae..6a023927 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/util/AndroidTimeUtils.kt @@ -10,17 +10,23 @@ import at.bitfire.ical4android.util.TimeApiExtensions import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.TemporalAmountAdapter +import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistry import net.fortuna.ical4j.model.property.DateListProperty import net.fortuna.ical4j.model.property.DateProperty +import net.fortuna.ical4j.model.property.RDate import java.text.SimpleDateFormat import java.time.Duration +import java.time.OffsetDateTime import java.time.Period +import java.time.temporal.ChronoField import java.time.temporal.TemporalAmount import java.util.LinkedList import java.util.Locale import java.util.logging.Logger +import kotlin.jvm.optionals.getOrDefault object AndroidTimeUtils { @@ -256,32 +262,55 @@ object AndroidTimeUtils { * * @return formatted string for Android calendar provider */ - fun recurrenceSetsToOpenTasksString(dates: List>, tz: net.fortuna.ical4j.model.TimeZone?): String { - TODO("ical4j 4.x") - - /*val allDay = tz == null - val strDates = LinkedList() + fun recurrenceSetsToOpenTasksString(dates: List>, tz: TimeZone?): String { + val allDay = tz == null + val strDatesBuilder = StringBuilder() for (dateListProp in dates) { - if (dateListProp is RDate && dateListProp.periods.isNotEmpty()) + if (dateListProp is RDate && dateListProp.periods.getOrDefault(emptyList()).isNotEmpty()) logger.warning("RDATE PERIOD not supported, ignoring") - for (date in dateListProp.dates) { - val dateToUse = - when (date) { - is DateTime if allDay -> // VALUE=DATE-TIME, but allDay=1 - Date(date) + fun Int.padWithZeros(length: Int = 2) = toString().padStart(length, '0') - !is DateTime if !allDay -> // VALUE=DATE, but allDay=0 - DateTime(date.toString(), tz) + for (date in dateListProp.dates) { + // The timezone is handled externally by a specific timezone column. We just need + // to use the datetime adjusted by this tz + val isUtc: Boolean = date.isSupported(ChronoField.OFFSET_SECONDS) && date.get(ChronoField.OFFSET_SECONDS) == 0 + val adjDate = if (!allDay && !TemporalAdapter.isFloating(date)) { + if (isUtc) + // UTC dates are not converted, they get 'Z' added at the end + date + else + OffsetDateTime.from(date).atZoneSameInstant(tz.toZoneId()) + } else { + date + } - else -> date + val sb = StringBuilder() + sb.append(adjDate.get(ChronoField.YEAR)) + sb.append(adjDate.get(ChronoField.MONTH_OF_YEAR).padWithZeros()) + sb.append(adjDate.get(ChronoField.DAY_OF_MONTH).padWithZeros()) + if (!allDay) { + sb.append('T') + if (adjDate.isSupported(ChronoField.HOUR_OF_DAY)) { + sb.append(adjDate.get(ChronoField.HOUR_OF_DAY).padWithZeros()) + sb.append(adjDate.get(ChronoField.MINUTE_OF_HOUR).padWithZeros()) + sb.append(adjDate.get(ChronoField.SECOND_OF_MINUTE).padWithZeros()) + } else { + // Time not supported - date doesn't have time (LocalDate) + // Force time to start of day + sb.append("000000") } - if (dateToUse is DateTime && !dateToUse.isUtc) - dateToUse.timeZone = tz!! - strDates += dateToUse.toString() + } + + // If the original date was UTC, append 'Z' at the end + if (isUtc) sb.append('Z') + + strDatesBuilder.append(sb) + strDatesBuilder.append(RECURRENCE_LIST_VALUE_SEPARATOR) } } - return strDates.joinToString(RECURRENCE_LIST_VALUE_SEPARATOR)*/ + // Remove suffix of RECURRENCE_LIST_VALUE_SEPARATOR to get rid of last added one + return strDatesBuilder.toString().removeSuffix(RECURRENCE_LIST_VALUE_SEPARATOR) } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt index 4c0188ff..33252673 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/util/AndroidTimeUtilsTest.kt @@ -6,34 +6,21 @@ package at.bitfire.synctools.util -import at.bitfire.ical4android.util.DateUtils import net.fortuna.ical4j.data.CalendarBuilder -import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.Parameter -import net.fortuna.ical4j.model.Period -import net.fortuna.ical4j.model.PeriodList import net.fortuna.ical4j.model.TimeZone import net.fortuna.ical4j.model.TimeZoneRegistryFactory -import net.fortuna.ical4j.model.component.VTimeZone -import net.fortuna.ical4j.model.parameter.TzId -import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.DateListProperty -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.ExDate import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import java.io.StringReader import java.time.Duration +import java.time.LocalDate +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.temporal.Temporal -@Ignore("ical4j 4.x") class AndroidTimeUtilsTest { val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()!! @@ -63,10 +50,6 @@ class AndroidTimeUtilsTest { val tzIdDefault = java.util.TimeZone.getDefault().id!! val tzDefault = tzRegistry.getTimeZone(tzIdDefault)!! - init { - TODO("ical4j 4.x") - } - // androidifyTimeZone // DateListProperty - date @@ -385,52 +368,64 @@ class AndroidTimeUtilsTest { "20150101T103010Z,20150102T103020Z", AndroidTimeUtils.recurrenceSetsToAndroidString(list, DateTime("20150101T103010ZZ")) ) - } + }*/ // recurrenceSetsToOpenTasksString @Test fun testRecurrenceSetsToOpenTasksString_UtcTimes() { - val list = ArrayList(1) - list.add(RDate(DateList("20150101T060000Z,20150702T060000Z", Value.DATE_TIME))) + val list = ArrayList>(1) + list.add(RDate(DateList( + ZonedDateTime.of(2015, 1, 1, 6, 0, 0, 0, ZoneOffset.UTC), + ZonedDateTime.of(2015, 7, 2, 6, 0, 0, 0, ZoneOffset.UTC) + ))) assertEquals("20150101T060000Z,20150702T060000Z", AndroidTimeUtils.recurrenceSetsToOpenTasksString(list, tzBerlin)) } @Test fun testRecurrenceSetsToOpenTasksString_ZonedTimes() { - val list = ArrayList(1) - list.add(RDate(DateList("20150101T060000,20150702T060000", Value.DATE_TIME, tzToronto))) + val list = ArrayList>(1) + list.add(RDate(DateList( + ZonedDateTime.of(2015, 1, 1, 6, 0, 0, 0, tzToronto.toZoneId()), + ZonedDateTime.of(2015, 7, 2, 6, 0, 0, 0, tzToronto.toZoneId()) + ))) assertEquals("20150101T120000,20150702T120000", AndroidTimeUtils.recurrenceSetsToOpenTasksString(list, tzBerlin)) } @Test fun testRecurrenceSetsToOpenTasksString_MixedTimes() { - val list = ArrayList(1) - list.add(RDate(DateList("20150101T060000Z,20150702T060000", Value.DATE_TIME, tzToronto))) + val list = ArrayList>(1) + list.add(RDate(DateList( + ZonedDateTime.of(2015, 1, 1, 1, 0, 0, 0, tzToronto.toZoneId()), + ZonedDateTime.of(2015, 7, 2, 6, 0, 0, 0, tzToronto.toZoneId()) + ))) assertEquals("20150101T070000,20150702T120000", AndroidTimeUtils.recurrenceSetsToOpenTasksString(list, tzBerlin)) } @Test fun testRecurrenceSetsToOpenTasksString_TimesAlthougAllDay() { - val list = ArrayList(1) - list.add(RDate(DateList("20150101T060000,20150702T060000", Value.DATE_TIME, tzToronto))) + val list = ArrayList>(1) + list.add(RDate(DateList( + ZonedDateTime.of(2015, 1, 1, 6, 0, 0, 0, tzToronto.toZoneId()), + ZonedDateTime.of(2015, 7, 2, 6, 0, 0, 0, tzToronto.toZoneId()) + ))) assertEquals("20150101,20150702", AndroidTimeUtils.recurrenceSetsToOpenTasksString(list, null)) } @Test fun testRecurrenceSetsToOpenTasksString_Dates() { - val list = ArrayList(1) - list.add(RDate(DateList("20150101,20150702", Value.DATE))) + val list = ArrayList>(1) + list.add(RDate(DateList(LocalDate.of(2015, 1, 1), LocalDate.of(2015, 7, 2)))) assertEquals("20150101,20150702", AndroidTimeUtils.recurrenceSetsToOpenTasksString(list, null)) } @Test fun testRecurrenceSetsToOpenTasksString_DatesAlthoughTimeZone() { - val list = ArrayList(1) - list.add(RDate(DateList("20150101,20150702", Value.DATE))) + val list = ArrayList>(1) + list.add(RDate(DateList(LocalDate.of(2015, 1, 1), LocalDate.of(2015, 7, 2)))) assertEquals("20150101T000000,20150702T000000", AndroidTimeUtils.recurrenceSetsToOpenTasksString(list, tzBerlin)) - }*/ + } @Test From 2992d01ab4fbc0e5cb897c65c1344533bc91f701 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 16 Mar 2026 14:22:40 +0100 Subject: [PATCH 13/24] [ical4j 4.x] Migrate event builders part 2 (#229) * Migrate `StartTimeBuilder` * Migrate `EndTimeBuilder` --- .../at/bitfire/ical4android/util/DateUtils.kt | 12 + .../calendar/builder/AndroidTemporalMapper.kt | 83 ++++++ .../calendar/builder/EndTimeBuilder.kt | 157 +++++------ .../calendar/builder/StartTimeBuilder.kt | 39 +-- .../test/kotlin/at/bitfire/Ical4jHelper.kt | 14 +- .../ical4android/util/DateUtilsTest.kt | 5 +- .../builder/AndroidTemporalMapperTest.kt | 179 +++++++++++++ .../calendar/builder/EndTimeBuilderTest.kt | 250 ++++++++++-------- .../calendar/builder/StartTimeBuilderTest.kt | 36 +-- 9 files changed, 516 insertions(+), 259 deletions(-) create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapper.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapperTest.kt diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt index 2d9dfaf1..9e2f56a5 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt @@ -65,6 +65,18 @@ object DateUtils { fun isDateTime(date: DateProperty<*>?): Boolean = date != null && TemporalAdapter.isDateTimePrecision(date.date) + /** + * Determines whether a given [Temporal] represents a DATE value. + */ + fun isDate(date: Temporal?): Boolean = + date != null && !TemporalAdapter.isDateTimePrecision(date) + + /** + * Determines whether a given [Temporal] represents a DATE-TIME value. + */ + fun isDateTime(date: Temporal?): Boolean = + date != null && TemporalAdapter.isDateTimePrecision(date) + /** * Parses an iCalendar that only contains a `VTIMEZONE` definition to a VTimeZone object. * diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapper.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapper.kt new file mode 100644 index 00000000..3ed70ba2 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapper.kt @@ -0,0 +1,83 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.calendar.builder + +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate +import net.fortuna.ical4j.model.TemporalAdapter +import net.fortuna.ical4j.util.TimeZones +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.Temporal + +object AndroidTemporalMapper { + + private const val TZID_UTC = "UTC" + + /** + * Converts this [Temporal] to the timestamp that should be used when writing an event to the + * Android calendar provider. + */ + fun Temporal.toTimestamp(): Long { + val epochSeconds = when (this) { + is LocalDate -> atStartOfDay().atZone(TimeZones.getDateTimeZone().toZoneId()).toEpochSecond() + is LocalDateTime -> atZone(TimeZones.getDefault().toZoneId()).toEpochSecond() + is OffsetDateTime -> toEpochSecond() + is ZonedDateTime -> toEpochSecond() + is Instant -> epochSecond + else -> error("Unsupported Temporal type: ${this::class.qualifiedName}") + } + + return epochSeconds * 1000L + } + + /** + * Converts this [Temporal] to a [ZonedDateTime] that is created from the timestamp returned by + * [toTimestamp] and the time zone returned by [androidTimezoneId]. + */ + fun Temporal.toZonedDateTime(): ZonedDateTime { + return Instant.ofEpochMilli(toTimestamp()).atZone(ZoneId.of(androidTimezoneId())) + } + + /** + * Returns the timezone ID that should be used when writing an event to the Android calendar provider. + * + * Note: For date-times with a given time zone, it needs to be a system time zone. Call + * [at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate] on dates coming from + * ical4j before calling this function. + * + * @return - "UTC" for dates and UTC date-times + * - the specified time zone ID for date-times with given time zone + * - the currently set default time zone ID for floating date-times + */ + fun Temporal.androidTimezoneId(): String { + return if (TemporalAdapter.isDateTimePrecision(this)) { + if (TemporalAdapter.isUtc(this)) { + TZID_UTC + } else if (TemporalAdapter.isFloating(this)) { + ZoneId.systemDefault().id + } else { + require(this is ZonedDateTime) { "Non-floating date-time must be a ZonedDateTime" } + + val timezoneId = this.zone.id + require(!timezoneId.startsWith("ical4j")) { + "ical4j ZoneIds are not supported. Call DatePropertyTzMapper.normalizedDate() " + + "before passing a date to this function." + } + + timezoneId + } + } else { + // For all-day events EventsColumns.EVENT_TIMEZONE must be "UTC". + TZID_UTC + } + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt index aaf65405..4a9c8492 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilder.kt @@ -11,25 +11,22 @@ import android.provider.CalendarContract.Events import androidx.annotation.VisibleForTesting import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.TimeApiExtensions.abs -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.toZonedDateTime +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.requireDtStart -import at.bitfire.synctools.util.AndroidTimeUtils -import net.fortuna.ical4j.model.DateTime +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.androidTimezoneId +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.component.VEvent -import net.fortuna.ical4j.model.property.DtEnd -import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RDate import net.fortuna.ical4j.model.property.RRule import java.time.Duration import java.time.LocalDate import java.time.Period -import java.time.ZoneId import java.time.ZonedDateTime +import java.time.temporal.Temporal import java.time.temporal.TemporalAmount +import kotlin.jvm.optionals.getOrNull class EndTimeBuilder: AndroidEntityBuilder { @@ -41,25 +38,24 @@ class EndTimeBuilder: AndroidEntityBuilder { - DURATION when the event is recurring. So we'll skip if this event is a recurring main event (only main events can be recurring). */ - TODO("ical4j 4.x") - /*val rRules = from.getProperties(Property.RRULE) - val rDates = from.getProperties(Property.RDATE) + val rRules = from.getProperties>(Property.RRULE) + val rDates = from.getProperties>(Property.RDATE) if (from === main && (rRules.isNotEmpty() || rDates.isNotEmpty())) { values.putNull(Events.DTEND) return } - val dtStart = from.requireDtStart() + val startDate = from.requireDtStart().normalizedDate() // potentially calculate DTEND from DTSTART + DURATION, and always align with DTSTART value type - val calculatedDtEnd = from.getEndDate(/* don't let ical4j calculate DTEND from DURATION */ false) - ?.let { alignWithDtStart(it, dtStart = dtStart) } - ?: calculateFromDuration(dtStart, from.duration?.duration) + val calculatedEndDate = from.getEndDate(/* deriveFromDuration = */ false).getOrNull()?.normalizedDate() + ?.let { alignWithDtStart(endDate = it, startDate = startDate) } + ?: calculateFromDuration(startDate, from.duration?.duration) // ignore DTEND when not after DTSTART and use default duration, if necessary - val dtEnd = calculatedDtEnd - ?.takeIf { it.date.toInstant() > dtStart.date.toInstant() } // only use DTEND if it's after DTSTART [1] - ?: calculateFromDefault(dtStart) + val endDate = calculatedEndDate + ?.takeIf { it.toTimestamp() > startDate.toTimestamp() } // only use DTEND if it's after DTSTART [1] + ?: calculateFromDefault(startDate) /** * [1] RFC 5545 3.8.2.2 Date-Time End: @@ -67,40 +63,18 @@ class EndTimeBuilder: AndroidEntityBuilder { */ // end time: UNIX timestamp - values.put(Events.DTEND, dtEnd.date.time) + values.put(Events.DTEND, endDate.toTimestamp()) // end time: timezone ID - if (DateUtils.isDateTime(dtEnd)) { - /* DTEND is a DATE-TIME. This can be: - - date/time with timezone ID ("DTEND;TZID=Europe/Vienna:20251006T155623") - - UTC ("DTEND:20251006T155623Z") - - floating time ("DTEND:20251006T155623") */ - - if (dtEnd.isUtc) { - // UTC - values.put(Events.EVENT_END_TIMEZONE, AndroidTimeUtils.TZID_UTC) - - } else if (dtEnd.timeZone != null) { - // timezone reference – make sure that time zone is known by Android - values.put(Events.EVENT_END_TIMEZONE, DateUtils.findAndroidTimezoneID(dtEnd.timeZone.id)) - - } else { - // floating time, use system default - values.put(Events.EVENT_END_TIMEZONE, ZoneId.systemDefault().id) - } - - } else { - // DTEND is a DATE - values.put(Events.EVENT_END_TIMEZONE, AndroidTimeUtils.TZID_UTC) - }*/ + values.put(Events.EVENT_END_TIMEZONE, endDate.androidTimezoneId()) } /** * Aligns the given DTEND to the VALUE-type (DATE-TIME/DATE) of DTSTART. * - * @param dtEnd DTEND to be aligned - * @param dtStart DTSTART to compare with + * @param endDate DTEND date to be aligned + * @param startDate DTSTART date to compare with * * @return * @@ -111,69 +85,72 @@ class EndTimeBuilder: AndroidEntityBuilder { * @see at.bitfire.synctools.mapping.calendar.handler.RecurrenceFieldsHandler.alignUntil */ @VisibleForTesting - internal fun alignWithDtStart(dtEnd: DtEnd<*>, dtStart: DtStart<*>): DtEnd<*> { - TODO("ical4j 4.x") - /*if (DateUtils.isDate(dtEnd)) { + internal fun alignWithDtStart(endDate: Temporal, startDate: Temporal): Temporal { + return if (endDate is LocalDate) { // DTEND is DATE - if (DateUtils.isDate(dtStart)) { + if (DateUtils.isDate(startDate)) { // DTEND is DATE, DTSTART is DATE - return dtEnd + endDate } else { // DTEND is DATE, DTSTART is DATE-TIME → amend with time and timezone - val endDate = dtEnd.date.toLocalDate() - val startTime = (dtStart.date as DateTime).toZonedDateTime() - val endDateWithTime = ZonedDateTime.of(endDate, startTime.toLocalTime(), startTime.zone) - return DtEnd(endDateWithTime.toIcal4jDateTime()) + val startZonedDateTime = startDate.toZonedDateTime() + + ZonedDateTime.of( + endDate, + startZonedDateTime.toLocalTime(), + startZonedDateTime.zone + ) } } else { // DTEND is DATE-TIME - if (DateUtils.isDate(dtStart)) { + if (DateUtils.isDate(startDate)) { // DTEND is DATE-TIME, DTSTART is DATE → only take date part - val endDate = dtEnd.date.toLocalDate() - return DtEnd(endDate.toIcal4jDate()) + endDate.toZonedDateTime().toLocalDate() } else { // DTEND is DATE-TIME, DTSTART is DATE-TIME - return dtEnd + endDate } - }*/ + } } /** - * Calculates the DTEND from DTSTART + DURATION, if possible. + * Calculates the DTEND date from DTSTART date + DURATION, if possible. * - * @param dtStart start date/date-time + * @param startDate start date/date-time * @param duration (optional) duration * - * @return end date/date-time (same value type as [dtStart]) or `null` if [duration] was not given + * @return end date/date-time (same value type as [startDate]) or `null` if [duration] was not given */ @VisibleForTesting - internal fun calculateFromDuration(dtStart: DtStart<*>, duration: TemporalAmount?): DtEnd<*>? { + internal fun calculateFromDuration(startDate: Temporal, duration: TemporalAmount?): Temporal? { if (duration == null) return null val dur = duration.abs() // always take positive temporal amount - TODO("ical4j 4.x") - /*return if (DateUtils.isDate(dtStart)) { + return if (DateUtils.isDate(startDate)) { // DTSTART is DATE - if (dur is Period) { - // date-based amount of time ("4 days") - val result = dtStart.date.toLocalDate() + dur - DtEnd(result.toIcal4jDate()) - } else if (dur is Duration) { - // time-based amount of time ("34 minutes") - val days = dur.toDays() - val result = dtStart.date.toLocalDate() + Period.ofDays(days.toInt()) - DtEnd(result.toIcal4jDate()) - } else - throw IllegalStateException() // TemporalAmount neither Period nor Duration - + when (dur) { + is Period -> { + // date-based amount of time ("4 days") + startDate + dur + } + + is Duration -> { + // time-based amount of time ("34 minutes") + val days = dur.toDays() + startDate + Period.ofDays(days.toInt()) + } + + else -> { + throw IllegalArgumentException("duration argument is neither Period nor Duration") + } + } } else { // DTSTART is DATE-TIME // We can add both date-based (Period) and time-based (Duration) amounts of time to an exact date/time. - val result = (dtStart.date as DateTime).toZonedDateTime() + dur - DtEnd(result.toIcal4jDateTime()) - }*/ + startDate.toZonedDateTime() + dur + } } /** @@ -186,26 +163,24 @@ class EndTimeBuilder: AndroidEntityBuilder { * > component specifies a "DTSTART" property with a DATE-TIME value type but no "DTEND" property, the event * > ends on the same calendar date and time of day specified by the "DTSTART" property. * - * In iCalendar, `DTEND` is non-inclusive at must be at a later time than `DTEND`. However in Android we can use + * In iCalendar, `DTEND` is non-inclusive and must be at a later time than `DTEND`. However, in Android we can use * the same value for both the `DTSTART` and the `DTEND` field, and so we use this to indicate a missing DTEND in * the original iCalendar. * - * @param dtStart start time to calculate end time from + * @param startDate start time to calculate end time from * @return End time to use for content provider: * - * - when [dtStart] is a `DATE`: [dtStart] + 1 day - * - when [dtStart] is a `DATE-TIME`: [dtStart] + * - when [startDate] is a `DATE`: [startDate] + 1 day + * - when [startDate] is a `DATE-TIME`: [startDate] */ @VisibleForTesting - internal fun calculateFromDefault(dtStart: DtStart<*>): DtEnd<*> = - TODO("ical4j 4.x") - /*if (DateUtils.isDate(dtStart)) { + internal fun calculateFromDefault(startDate: Temporal): Temporal = + if (startDate is LocalDate) { // DATE → one day duration - val endDate: LocalDate = dtStart.date.toLocalDate().plusDays(1) - DtEnd(endDate.toIcal4jDate()) + startDate.plusDays(1) } else { // DATE-TIME → same as DTSTART to indicate there was no DTEND set - DtEnd(dtStart.value, dtStart.timeZone) - }*/ + startDate + } } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt index 082d07b9..15ea9822 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilder.kt @@ -8,47 +8,26 @@ package at.bitfire.synctools.mapping.calendar.builder import android.content.Entity import android.provider.CalendarContract.Events -import at.bitfire.ical4android.util.DateUtils +import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate import at.bitfire.synctools.icalendar.requireDtStart -import at.bitfire.synctools.util.AndroidTimeUtils +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.androidTimezoneId +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp import net.fortuna.ical4j.model.component.VEvent -import java.time.ZoneId +import java.time.temporal.Temporal class StartTimeBuilder: AndroidEntityBuilder { override fun build(from: VEvent, main: VEvent, to: Entity) { - TODO("ical4j 4.x") - /*val values = to.entityValues + val values = to.entityValues - val dtStart = from.requireDtStart() + val dtStart = from.requireDtStart() + val normalizedDate = dtStart.normalizedDate() // start time: UNIX timestamp - values.put(Events.DTSTART, dtStart.date.time) + values.put(Events.DTSTART, normalizedDate.toTimestamp()) // start time: timezone ID - if (DateUtils.isDateTime(dtStart)) { - /* DTSTART is a DATE-TIME. This can be: - - date/time with timezone ID ("DTSTART;TZID=Europe/Vienna:20251006T155623") - - UTC ("DTSTART:20251006T155623Z") - - floating time ("DTSTART:20251006T155623") */ - - if (dtStart.isUtc) { - // UTC - values.put(Events.EVENT_TIMEZONE, AndroidTimeUtils.TZID_UTC) - - } else if (dtStart.timeZone != null) { - // timezone reference – make sure that time zone is known by Android - values.put(Events.EVENT_TIMEZONE, DateUtils.findAndroidTimezoneID(dtStart.timeZone.id)) - - } else { - // floating time, use system default - values.put(Events.EVENT_TIMEZONE, ZoneId.systemDefault().id) - } - - } else { - // DTSTART is a DATE - values.put(Events.EVENT_TIMEZONE, AndroidTimeUtils.TZID_UTC) - }*/ + values.put(Events.EVENT_TIMEZONE, normalizedDate.androidTimezoneId()) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt b/lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt index 6a23e6fd..a54a03e9 100644 --- a/lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt +++ b/lib/src/test/kotlin/at/bitfire/Ical4jHelper.kt @@ -9,7 +9,9 @@ package at.bitfire import net.fortuna.ical4j.model.CalendarDateFormat import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.TimeZone +import java.time.LocalDate import java.time.LocalDateTime +import java.time.ZoneId import java.time.ZonedDateTime import java.time.temporal.Temporal @@ -22,10 +24,18 @@ fun dateTimeValue(value: String): Temporal { } fun dateTimeValue(value: String, timeZone: TimeZone): ZonedDateTime { + return dateTimeValue(value, timeZone.toZoneId()) +} + +fun dateTimeValue(value: String, zone: ZoneId): ZonedDateTime { val temporal = dateTimeValue(value) return if (temporal is LocalDateTime) { - temporal.atZone(timeZone.toZoneId()) + temporal.atZone(zone) } else { error("Unexpected temporal type: ${temporal::class}") } -} \ No newline at end of file +} + +fun dateValue(value: String): LocalDate { + return TemporalAdapter.parse(value, CalendarDateFormat.DATE_FORMAT).temporal +} diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt index 828b1614..87512cfd 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt @@ -8,6 +8,7 @@ package at.bitfire.ical4android.util import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import org.junit.Assert.assertEquals @@ -45,7 +46,7 @@ class DateUtilsTest { TODO("ical4j 4.x") /*assertTrue(DateUtils.isDate(DtStart(Date("20200101")))) assertFalse(DateUtils.isDate(DtStart(DateTime("20200101T010203Z"))))*/ - assertFalse(DateUtils.isDate(null)) + assertFalse(DateUtils.isDate(null as DateProperty<*>?)) } @Test @@ -53,7 +54,7 @@ class DateUtilsTest { TODO("ical4j 4.x") /*assertFalse(DateUtils.isDateTime(DtEnd(Date("20200101")))) assertTrue(DateUtils.isDateTime(DtEnd(DateTime("20200101T010203Z"))))*/ - assertFalse(DateUtils.isDateTime(null)) + assertFalse(DateUtils.isDateTime(null as DateProperty<*>?)) } diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapperTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapperTest.kt new file mode 100644 index 00000000..29f1c488 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/AndroidTemporalMapperTest.kt @@ -0,0 +1,179 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.mapping.calendar.builder + +import at.bitfire.DefaultTimezoneRule +import at.bitfire.synctools.icalendar.requireDtStart +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.androidTimezoneId +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toTimestamp +import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.component.VEvent +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import java.io.StringReader +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.chrono.JapaneseDate +import java.time.temporal.Temporal + +class AndroidTemporalMapperTest { + + @get:Rule + val tzRule = DefaultTimezoneRule("Europe/Vienna") + + @Test + fun `toTimestamp on LocalDate should use start of UTC day`() { + val date = LocalDate.of(2026, 3, 12) + + val timestamp = date.toTimestamp() + + assertEquals(1773273600000L, timestamp) + } + + @Test + fun `toTimestamp on LocalDateTime should use system default time zone`() { + val date = LocalDateTime.of(2026, 3, 12, 12, 34, 56) + + val timestamp = date.toTimestamp() + + assertEquals(1773315296000L, timestamp) + } + + @Test + fun `toTimestamp on OffsetDateTime`() { + val date = OffsetDateTime.of(2026, 3, 12, 12, 0, 0, 0, ZoneOffset.ofHours(3)) + + val timestamp = date.toTimestamp() + + assertEquals(1773306000000L, timestamp) + } + + @Test + fun `toTimestamp on ZonedDateTime`() { + val date = ZonedDateTime.of(2026, 3, 12, 12, 0, 0, 0, ZoneId.of("Europe/Helsinki")) + + val timestamp = date.toTimestamp() + + assertEquals(1773309600000L, timestamp) + } + + @Test + fun `toTimestamp on Instant`() { + val inputTimestamp = 1773273600000L + val date = Instant.ofEpochMilli(inputTimestamp) + + val timestamp = date.toTimestamp() + + assertEquals(inputTimestamp, timestamp) + } + + @Test + fun `toTimestamp on unsupported type`() { + try { + JapaneseDate.now().toTimestamp() + + fail("Expected exception") + } catch (e: IllegalStateException) { + assertEquals("Unsupported Temporal type: java.time.chrono.JapaneseDate", e.message) + } + } + + + @Test + fun `androidTimezoneId on LocalDate`() { + val date = LocalDate.now() + + val timezoneId = date.androidTimezoneId() + + assertEquals("UTC", timezoneId) + } + + @Test + fun `androidTimezoneId on LocalDateTime`() { + val date = LocalDateTime.now() + + val timezoneId = date.androidTimezoneId() + + assertEquals(tzRule.defaultZoneId.id, timezoneId) + } + + @Test + fun `androidTimezoneId on ZonedDateTime`() { + val date = LocalDateTime.now().atZone(ZoneId.of("Europe/Dublin")) + + val timezoneId = date.androidTimezoneId() + + assertEquals("Europe/Dublin", timezoneId) + } + + @Test + fun `androidTimezoneId on Instant`() { + val date = Instant.now() + + val timezoneId = date.androidTimezoneId() + + assertEquals("UTC", timezoneId) + } + + @Test + fun `androidTimezoneId on OffsetDateTime`() { + try { + OffsetDateTime.now().androidTimezoneId() + + fail("Expected exception") + } catch (e: IllegalArgumentException) { + assertEquals("Non-floating date-time must be a ZonedDateTime", e.message) + } + } + + @Test + fun `androidTimezoneId on ZonedDateTime from ical4j`() { + val cal = CalendarBuilder().build(StringReader( + """ + BEGIN:VCALENDAR + VERSION:2.0 + BEGIN:VTIMEZONE + TZID:Etc/ABC + BEGIN:STANDARD + TZNAME:-03 + TZOFFSETFROM:-0300 + TZOFFSETTO:-0300 + DTSTART:19700101T000000 + END:STANDARD + END:VTIMEZONE + BEGIN:VEVENT + SUMMARY:Test Timezones + DTSTART;TZID=Etc/ABC:20250828T130000 + END:VEVENT + END:VCALENDAR + """.trimIndent() + )) + val vEvent = cal.getComponent(Component.VEVENT).get() + val date = vEvent.requireDtStart().date + + try { + date.androidTimezoneId() + + fail("Expected exception") + } catch (e: IllegalArgumentException) { + assertEquals( + "ical4j ZoneIds are not supported. Call DatePropertyTzMapper.normalizedDate() " + + "before passing a date to this function.", + e.message + ) + } + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt index 5b6c1960..2e414f4e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/EndTimeBuilderTest.kt @@ -9,9 +9,10 @@ package at.bitfire.synctools.mapping.calendar.builder import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract.Events +import at.bitfire.DefaultTimezoneRule +import at.bitfire.dateTimeValue +import at.bitfire.dateValue import at.bitfire.synctools.icalendar.propertyListOf -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtEnd @@ -21,34 +22,31 @@ import net.fortuna.ical4j.model.property.RRule import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Ignore +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.Period -import java.time.ZoneId +import java.time.temporal.Temporal -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class EndTimeBuilderTest { + @get:Rule + val tzRule = DefaultTimezoneRule("Europe/Berlin") + private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzDefault = tzRegistry.getTimeZone(ZoneId.systemDefault().id) private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna") private val builder = EndTimeBuilder() - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun `Recurring event`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")), - DtEnd(Date("20251011")), - RRule("FREQ=DAILY;COUNT=5") + DtStart(dateValue("20251010")), + DtEnd(dateValue("20251011")), + RRule("FREQ=DAILY;COUNT=5") )) builder.build(event, event, result) assertTrue(result.entityValues.containsKey(Events.DTEND)) @@ -60,274 +58,294 @@ class EndTimeBuilderTest { fun `Non-recurring all-day event (with DTEND)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")), - DtEnd(Date("20251011")) + DtStart(dateValue("20251010")), + DtEnd(dateValue("20251011")) )) builder.build(event, event, result) assertEquals(1760140800000, result.entityValues.get(Events.DTEND)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring all-day event (with DTEND before DTSTART)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")), - DtEnd(Date("20251001")) // before DTSTART, should be ignored + DtStart(dateValue("20251010")), + DtEnd(dateValue("20251001")) // before DTSTART, should be ignored )) builder.build(event, event, result) // default duration: one day → 20251011 assertEquals(1760140800000, result.entityValues.get(Events.DTEND)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring all-day event (with DTEND equals DTSTART)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")), - DtEnd(Date("20251010")) // equals DTSTART, should be ignored + DtStart(dateValue("20251010")), + DtEnd(dateValue("20251010")) // equals DTSTART, should be ignored )) builder.build(event, event, result) // default duration: one day → 20251011 assertEquals(1760140800000, result.entityValues.get(Events.DTEND)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (with floating DTEND)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203", tzVienna)), - DtEnd(DateTime("20251011T040506")) + DtStart(dateTimeValue("20251010T010203", tzVienna)), + DtEnd(dateTimeValue("20251011T040506")) )) builder.build(event, event, result) - assertEquals(DateTime("20251011T040506", tzDefault).time, result.entityValues.get(Events.DTEND)) + assertEquals(1760148306000L, result.entityValues.get(Events.DTEND)) + assertEquals(tzRule.defaultZoneId.id, result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (with UTC DTEND)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203", tzVienna)), - DtEnd(DateTime("20251011T040506Z")) + DtStart(dateTimeValue("20251010T010203", tzVienna)), + DtEnd(dateTimeValue("20251011T040506Z")) )) builder.build(event, event, result) assertEquals(1760155506000, result.entityValues.get(Events.DTEND)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (with zoned DTEND)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203", tzVienna)), - DtEnd(DateTime("20251011T040506", tzVienna)) + DtStart(dateTimeValue("20251010T010203", tzVienna)), + DtEnd(dateTimeValue("20251011T040506", tzVienna)) )) builder.build(event, event, result) assertEquals(1760148306000, result.entityValues.get(Events.DTEND)) + assertEquals("Europe/Vienna", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (with zoned DTEND before DTSTART)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251011T040506", tzVienna)), - DtEnd(DateTime("20251010T040506", tzVienna)) // before DTSTART, should be ignored + DtStart(dateTimeValue("20251011T040506", tzVienna)), + DtEnd(dateTimeValue("20251010T040506", tzVienna)) // before DTSTART, should be ignored )) builder.build(event, event, result) // default duration: 0 sec -> DTEND == DTSTART in calendar provider assertEquals(1760148306000, result.entityValues.get(Events.DTEND)) + assertEquals("Europe/Vienna", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (with zoned DTEND equals DTSTART)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251011T040506", tzVienna)), - DtEnd(DateTime("20251011T040506", tzVienna)) // equals DTSTART, should be ignored + DtStart(dateTimeValue("20251011T040506", tzVienna)), + DtEnd(dateTimeValue("20251011T040506", tzVienna)) // equals DTSTART, should be ignored )) builder.build(event, event, result) // default duration: 0 sec -> DTEND == DTSTART in calendar provider assertEquals(1760148306000, result.entityValues.get(Events.DTEND)) + assertEquals("Europe/Vienna", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring all-day event (with DURATION)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")), + DtStart(dateValue("20251010")), Duration(Period.ofDays(3)) )) builder.build(event, event, result) assertEquals(1760313600000, result.entityValues.get(Events.DTEND)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring all-day event (with negative DURATION)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")), + DtStart(dateValue("20251010")), Duration(Period.ofDays(-3)) // invalid negative DURATION will be treated as positive )) builder.build(event, event, result) assertEquals(1760313600000, result.entityValues.get(Events.DTEND)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (with DURATION)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203", tzVienna)), + DtStart(dateTimeValue("20251010T010203", tzVienna)), Duration(java.time.Duration.ofMinutes(90)) )) builder.build(event, event, result) assertEquals(1760056323000, result.entityValues.get(Events.DTEND)) + assertEquals("Europe/Vienna", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (with negative DURATION)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203", tzVienna)), + DtStart(dateTimeValue("20251010T010203", tzVienna)), Duration(java.time.Duration.ofMinutes(-90)) // invalid negative DURATION will be treated as positive )) builder.build(event, event, result) assertEquals(1760056323000, result.entityValues.get(Events.DTEND)) + assertEquals("Europe/Vienna", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring all-day event (neither DTEND nor DURATION)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")) + DtStart(dateValue("20251010")) )) builder.build(event, event, result) // default duration 1 day assertEquals(1760140800000, result.entityValues.get(Events.DTEND)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test fun `Non-recurring non-all-day event (neither DTEND nor DURATION)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203", tzVienna)) + DtStart(dateTimeValue("20251010T010203", tzVienna)) )) builder.build(event, event, result) // default duration 0 seconds assertEquals(1760050923000, result.entityValues.get(Events.DTEND)) + assertEquals("Europe/Vienna", result.entityValues.get(Events.EVENT_END_TIMEZONE)) } @Test - fun `alignWithDtStart(dtEnd=DATE, dtStart=DATE)`() { - val result = builder.alignWithDtStart( - DtEnd(Date("20251007")), - DtStart(Date("20250101")) - ) - assertEquals(DtEnd(Date("20251007")), result) + fun `alignWithDtStart(endDate=DATE, startDate=DATE)`() { + val endDate = dateValue("20251007") + val startDate = dateValue("20250101") + + val result = builder.alignWithDtStart(endDate, startDate) + + assertEquals(endDate, result) + } + + @Test + fun `alignWithDtStart(endDate=DATE, startDate=DATE-TIME)`() { + val endDate = dateValue("20251007") + val startDate = dateTimeValue("20250101T005623", tzVienna) + + val result = builder.alignWithDtStart(endDate, startDate) + + assertEquals(dateTimeValue("20251007T005623", tzVienna), result) } @Test - fun `alignWithDtStart(dtEnd=DATE, dtStart=DATE-TIME`() { - val result = builder.alignWithDtStart( - DtEnd(Date("20251007")), - DtStart(DateTime("20250101T005623", tzVienna)) - ) - assertEquals(DtEnd(DateTime("20251007T005623", tzVienna)), result) + fun `alignWithDtStart(endDate=DATE, startDate=DATE-TIME (floating))`() { + val endDate = dateValue("20251007") + val startDate = dateTimeValue("20250101T005623") + + val result = builder.alignWithDtStart(endDate, startDate) + + assertEquals(dateTimeValue("20251007T005623", tzRule.defaultZoneId), result) } @Test - fun `alignWithDtStart(dtEnd=DATE-TIME, dtStart=DATE)`() { - val result = builder.alignWithDtStart( - DtEnd(DateTime("20251007T010203Z")), - DtStart(Date("20250101")) - ) - assertEquals(DtEnd(Date("20251007")), result) + fun `alignWithDtStart(endDate=DATE-TIME, startDate=DATE)`() { + val endDate = dateTimeValue("20251007T010203Z") + val startDate = dateValue("20250101") + + val result = builder.alignWithDtStart(endDate, startDate) + + assertEquals(dateValue("20251007"), result) } @Test - fun `alignWithDtStart(dtEnd=DATE-TIME, dtStart=DATE-TIME)`() { - val result = builder.alignWithDtStart( - DtEnd(DateTime("20251007T010203Z")), - DtStart(DateTime("20250101T045623", tzVienna)) - ) - assertEquals(DtEnd(DateTime("20251007T010203Z")), result) + fun `alignWithDtStart(endDate=DATE-TIME, startDate=DATE-TIME)`() { + val endDate = dateTimeValue("20251007T010203Z") + val startDate = dateTimeValue("20250101T045623", tzVienna) + + val result = builder.alignWithDtStart(endDate, startDate) + + assertEquals(endDate, result) } @Test fun `calculateFromDefault (DATE)`() { - assertEquals( - DtEnd(Date("20251101")), - builder.calculateFromDefault(DtStart(Date("20251031"))) - ) + val startDate = dateValue("20251031") + + val result = builder.calculateFromDefault(startDate) + + assertEquals(dateValue("20251101"), result) } @Test fun `calculateFromDefault (DATE-TIME)`() { - val time = DateTime("20251031T123466Z") - assertEquals( - DtEnd(time), - builder.calculateFromDefault(DtStart(time)) - ) + val startDate = dateTimeValue("20251031T123456Z") + + val result = builder.calculateFromDefault(startDate) + + assertEquals(startDate, result) } @Test - fun `calculateFromDuration (dtStart=DATE, duration is date-based)`() { - val result = builder.calculateFromDuration( - DtStart(Date("20240228")), - java.time.Duration.ofDays(1) - ) - assertEquals( - DtEnd(Date("20240229")), // leap day - result - ) + fun `calculateFromDuration (startDate=DATE, duration is date-based)`() { + val startDate = dateValue("20240228") + val duration = java.time.Duration.ofDays(1) + + val result = builder.calculateFromDuration(startDate, duration) + + // leap day + assertEquals(dateValue("20240229"), result) } @Test - fun `calculateFromDuration (dtStart=DATE, duration is time-based)`() { - val result = builder.calculateFromDuration( - DtStart(Date("20241231")), - java.time.Duration.ofHours(25) - ) - assertEquals( - DtEnd(Date("20250101")), - result - ) + fun `calculateFromDuration (startDate=DATE, duration is time-based)`() { + val startDate = dateValue("20241231") + val duration = java.time.Duration.ofHours(25) + + val result = builder.calculateFromDuration(startDate, duration) + + assertEquals(dateValue("20250101"), result) } @Test - fun `calculateFromDuration (dtStart=DATE-TIME, duration is date-based)`() { - val result = builder.calculateFromDuration( - DtStart(DateTime("20250101T045623", tzVienna)), - java.time.Duration.ofDays(1) - ) - assertEquals( - DtEnd(DateTime("20250102T045623", tzVienna)), - result - ) + fun `calculateFromDuration (startDate=DATE-TIME, duration is date-based)`() { + val startDate = dateTimeValue("20250101T045623", tzVienna) + val duration = java.time.Duration.ofDays(1) + + val result = builder.calculateFromDuration(startDate, duration) + + assertEquals(dateTimeValue("20250102T045623", tzVienna), result) } @Test - fun `calculateFromDuration (dtStart=DATE-TIME, duration is time-based)`() { - val result = builder.calculateFromDuration( - DtStart(DateTime("20250101T045623", tzVienna)), - java.time.Duration.ofHours(25) - ) - assertEquals( - DtEnd(DateTime("20250102T055623", tzVienna)), - result - ) + fun `calculateFromDuration (startDate=DATE-TIME, duration is time-based)`() { + val startDate = dateTimeValue("20250101T045623", tzVienna) + val duration = java.time.Duration.ofHours(25) + + val result = builder.calculateFromDuration(startDate, duration) + + assertEquals(dateTimeValue("20250102T055623", tzVienna), result) } @Test - fun `calculateFromDuration (dtStart=DATE-TIME, duration is time-based and negative)`() { - val result = builder.calculateFromDuration( - DtStart(DateTime("20250101T045623", tzVienna)), - java.time.Duration.ofHours(-25) - ) - assertEquals( - DtEnd(DateTime("20250102T055623", tzVienna)), - result - ) - }*/ + fun `calculateFromDuration (startDate=DATE-TIME, duration is time-based and negative)`() { + val startDate = dateTimeValue("20250101T045623", tzVienna) + val duration = java.time.Duration.ofHours(-25) + + val result = builder.calculateFromDuration(startDate, duration) + + assertEquals(dateTimeValue("20250102T055623", tzVienna), result) + } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt index defcc647..582d2f3e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/builder/StartTimeBuilderTest.kt @@ -9,26 +9,27 @@ package at.bitfire.synctools.mapping.calendar.builder import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract.Events +import at.bitfire.DefaultTimezoneRule +import at.bitfire.dateTimeValue +import at.bitfire.dateValue import at.bitfire.synctools.exception.InvalidICalendarException import at.bitfire.synctools.icalendar.propertyListOf -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart import org.junit.Assert.assertEquals -import org.junit.Ignore +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import java.time.ZoneId -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class StartTimeBuilderTest { + @get:Rule + val tzRule = DefaultTimezoneRule("Europe/Berlin") + private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzDefault = tzRegistry.getTimeZone(ZoneId.systemDefault().id) private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna") private val builder = StartTimeBuilder() @@ -40,49 +41,48 @@ class StartTimeBuilderTest { builder.build(event, event, result) } - - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun `All-day event`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(Date("20251010")) + DtStart(dateValue("20251010")) )) builder.build(event, event, result) assertEquals(1760054400000, result.entityValues.get(Events.DTSTART)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_TIMEZONE)) } @Test fun `Non-all-day event (floating DTSTART)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203")) + DtStart(dateTimeValue("20251010T010203")) )) builder.build(event, event, result) - assertEquals(DateTime("20251010T010203", tzDefault).time, result.entityValues.get(Events.DTSTART)) + assertEquals(1760050923000L, result.entityValues.get(Events.DTSTART)) + assertEquals(tzRule.defaultZoneId.id, result.entityValues.get(Events.EVENT_TIMEZONE)) } @Test fun `Non-all-day event (UTC DTSTART)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203Z")) + DtStart(dateTimeValue("20251010T010203Z")) )) builder.build(event, event, result) assertEquals(1760058123000L, result.entityValues.get(Events.DTSTART)) + assertEquals("UTC", result.entityValues.get(Events.EVENT_TIMEZONE)) } @Test fun `Non-all-day event (zoned DTSTART)`() { val result = Entity(ContentValues()) val event = VEvent(propertyListOf( - DtStart(DateTime("20251010T010203", tzVienna)) + DtStart(dateTimeValue("20251010T010203", tzVienna)) )) builder.build(event, event, result) assertEquals(1760050923000, result.entityValues.get(Events.DTSTART)) - }*/ + assertEquals("Europe/Vienna", result.entityValues.get(Events.EVENT_TIMEZONE)) + } } \ No newline at end of file From d1b2d72c05aa8ed810f6ef138e044ad0fe0b90a5 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 17 Mar 2026 08:28:32 +0100 Subject: [PATCH 14/24] Drop `AndroidCompatTimeZoneRegistry` (#233) * [WIP] Refactor timezone handling in ICalendarGenerator - Rename `usedTimeZones` to `usedTimezoneIds` and change type from `Set` to `Set` - Update `timeZonesOf()` to extract TZID parameters instead of ZoneId from ZonedDateTime - Add recursive processing of subcomponents in `timeZonesOf()` - Add TODO comments for future improvements - Remove unused `toZonedDateTime()` helper function - Update imports and add `@VisibleForTesting` annotation - Add TODO comment in test file for missing tests * Remove AndroidCompatTimeZoneRegistry and related test files - Delete AndroidCompatTimeZoneRegistry implementation and factory - Remove AndroidCompatTimeZoneRegistryTest and Ical4jConfigurationTest - Update ical4j.properties to remove custom timezone registry - Remove ProGuard rules for AndroidCompatTimeZoneRegistry * Preserve original Android timezone IDs in iCalendar output - Add test case for Europe/Kiev timezone handling - Ensure VTIMEZONE TZID matches original Android timezone ID - Add logging for timezone ID mismatches between Android and ical4j - Update imports and add logger to ICalendarGenerator * Improve timezone handling in ICalendarGenerator - Add `copyVTimeZone` method to safely modify VTIMEZONE without affecting registry cache - Replace direct timezone modification with copy-based approach - Add test for `copyVTimeZone` to verify property list and observances isolation - Update timezone ID replacement test to use regex pattern matching - Add imports for new functionality * Add tests for `timeZonesOf` method in ICalendarGenerator - Implement tests for extracting TZIDs from date properties - Add test for empty result when no TZIDs present - Include test for TZID extraction from subcomponents - Add test for empty component case - Remove unused helper function `toZonedDateTime` * Add VTimeZoneMinifier class and move minifyVTimeZone logic - Introduce new `VTimeZoneMinifier` class with dedicated minification logic - Move `minifyVTimeZone` method from `ICalendar` to new class - Add comprehensive tests for `VTimeZoneMinifier` functionality - Remove now redundant test cases from `ICalendarTest` - Clean up imports and unused helper functions * Refactor VTimeZoneMinifier to support Temporal types - Rename `minifyVTimeZone` to `minify` and change parameter type from `ZonedDateTime?` to `Temporal?` - Add `asZonedDateTime` helper function to convert various Temporal types to ZonedDateTime - Update all test cases to use new method name and handle nullable start time - Fix typo in test comment - Integrate minifier into ICalendarGenerator * Improve VTimeZoneMinifier robustness and timezone handling - Enhance `minify` method to handle timezone conversion more robustly - Add fallback to system default timezone when original TZID is invalid - Remove redundant null checks and simplify logic flow - Fix test function name formatting - Update documentation to reflect parameter name changes - Clean up code comments and remove outdated TODOs - Add null safety for timezone registry lookups - Improve error handling in timezone minification validation - --- lib/consumer-rules.pro | 4 - .../AndroidCompatTimeZoneRegistryTest.kt | 103 ------------ .../AndroidCompatTimeZoneRegistry.kt | 107 ------------- .../at/bitfire/ical4android/ICalendar.kt | 106 ------------- .../synctools/icalendar/ICalendarGenerator.kt | 121 +++++++++----- .../synctools/icalendar/VTimeZoneMinifier.kt | 148 ++++++++++++++++++ lib/src/main/resources/ical4j.properties | 1 - .../at/bitfire/ical4android/ICalendarTest.kt | 118 -------------- .../ical4android/Ical4jConfigurationTest.kt | 25 --- .../icalendar/ICalendarGeneratorTest.kt | 126 +++++++++++++++ .../icalendar/VTimeZoneMinifierTest.kt | 136 ++++++++++++++++ 11 files changed, 496 insertions(+), 499 deletions(-) delete mode 100644 lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt delete mode 100644 lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt create mode 100644 lib/src/main/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifier.kt delete mode 100644 lib/src/test/kotlin/at/bitfire/ical4android/Ical4jConfigurationTest.kt create mode 100644 lib/src/test/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifierTest.kt diff --git a/lib/consumer-rules.pro b/lib/consumer-rules.pro index ba64faf6..e20589b2 100644 --- a/lib/consumer-rules.pro +++ b/lib/consumer-rules.pro @@ -10,10 +10,6 @@ -dontwarn org.codehaus.groovy.** -dontwarn org.jparsec.** -# keep to be used by ical4j --keep class at.bitfire.ical4android.AndroidCompatTimeZoneRegistry { *; } --keep class at.bitfire.ical4android.AndroidCompatTimeZoneRegistry$Factory { *; } - # keep all vCard properties/parameters (used via reflection) -keep class ezvcard.io.scribe.** { *; } -keep class ezvcard.property.** { *; } diff --git a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt b/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt deleted file mode 100644 index 2005bb07..00000000 --- a/lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package at.bitfire.ical4android - -import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory -import net.fortuna.ical4j.model.TimeZone -import net.fortuna.ical4j.model.TimeZoneRegistry -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assume -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import java.time.ZoneId -import java.time.zone.ZoneRulesException - -@Ignore("ical4j 4.x") -class AndroidCompatTimeZoneRegistryTest { - - lateinit var ical4jRegistry: TimeZoneRegistry - lateinit var registry: AndroidCompatTimeZoneRegistry - - private val systemKnowsKyiv = - try { - ZoneId.of("Europe/Kyiv") - true - } catch (e: ZoneRulesException) { - false - } - - @Before - fun createRegistry() { - ical4jRegistry = DefaultTimeZoneRegistryFactory().createRegistry() - registry = AndroidCompatTimeZoneRegistry.Factory().createRegistry() - } - - - @Test - fun getTimeZone_Existing() { - assertEquals( - ical4jRegistry.getTimeZone("Europe/Vienna"), - registry.getTimeZone("Europe/Vienna") - ) - } - - @Test - fun getTimeZone_Existing_ButNotInIcal4j() { - TODO("ical4j 4.x") - /*val reg = AndroidCompatTimeZoneRegistry(object: TimeZoneRegistry { - override fun register(timezone: TimeZone?) = throw NotImplementedError() - override fun register(timezone: TimeZone?, update: Boolean) = throw NotImplementedError() - override fun clear() = throw NotImplementedError() - override fun getTimeZone(id: String?) = null - - }) - assertNull(reg.getTimeZone("Europe/Berlin"))*/ - } - - @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_Copenhagen_NoBerlin() { - val tz = registry.getTimeZone("Europe/Copenhagen")!! - assertEquals("Europe/Copenhagen", tz.id) - assertFalse(tz.vTimeZone.toString().contains("Berlin")) - } - - @Test - fun getTimeZone_NotExisting() { - assertNull(registry.getTimeZone("Test/NotExisting")) - } - - @Test - fun getTimeZone_Empty() { - // Make sure that if an empty timezone is given, the function doesn't crash but returns null - assertNull(registry.getTimeZone("")) - } - -} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt deleted file mode 100644 index 3cfc9db0..00000000 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package at.bitfire.ical4android - -import net.fortuna.ical4j.model.ComponentList -import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory -import net.fortuna.ical4j.model.PropertyList -import net.fortuna.ical4j.model.TimeZone -import net.fortuna.ical4j.model.TimeZoneRegistry -import net.fortuna.ical4j.model.TimeZoneRegistryFactory -import net.fortuna.ical4j.model.TimeZoneRegistryImpl -import net.fortuna.ical4j.model.component.VTimeZone -import net.fortuna.ical4j.model.property.TzId -import java.time.ZoneId -import java.util.logging.Logger - -/** - * Wrapper around default [TimeZoneRegistry] that uses the Android name if a time zone has a - * different name in ical4j and Android. - * - * **This time zone registry is set as default registry for ical4android projects in - * resources/ical4j.properties.** - * - * 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 { - - private val logger - get() = Logger.getLogger(javaClass.name) - - /** - * 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? { - // If the timezone is empty, or format is not valid, return null - if (id.isEmpty()) return null - - val tz: TimeZone = try { - base.getTimeZone(id) ?: return null // ical4j doesn't know time zone, return null - } catch (_: NullPointerException) { - // The registry sometimes throws NullPointerException instead of returning null - return null - } - - // check whether time zone is available on Android, too - 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 tz - } - - /* 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. - */ - if (tz.id != androidTzId) { - logger.fine("Using ical4j timezone ${tz.id} data to construct Android timezone $androidTzId") - - // create a copy of the VTIMEZONE so that we don't modify the original registry values (which are not immutable) - return TimeZone( - VTimeZone( - PropertyList(listOf(TzId(androidTzId))), - ComponentList(tz.vTimeZone.observances) - ) - ) - } else - return tz - } - - - class Factory : TimeZoneRegistryFactory() { - - override fun createRegistry(): AndroidCompatTimeZoneRegistry { - val ical4jRegistry = DefaultTimeZoneRegistryFactory().createRegistry() - return AndroidCompatTimeZoneRegistry(ical4jRegistry) - } - - } - -} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt index dcd7ffd4..f74d5510 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt @@ -10,18 +10,12 @@ import at.bitfire.ical4android.ICalendar.Companion.CALENDAR_NAME import at.bitfire.synctools.BuildConfig import at.bitfire.synctools.exception.InvalidICalendarException import at.bitfire.synctools.icalendar.ICalendarParser -import at.bitfire.synctools.icalendar.propertyListOf import at.bitfire.synctools.icalendar.validation.ICalPreprocessor import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.Calendar -import net.fortuna.ical4j.model.ComponentList import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.Property -import net.fortuna.ical4j.model.TemporalAdapter -import net.fortuna.ical4j.model.component.Daylight -import net.fortuna.ical4j.model.component.Observance -import net.fortuna.ical4j.model.component.Standard import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Related @@ -29,15 +23,12 @@ import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId -import net.fortuna.ical4j.model.property.RDate -import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.Trigger import net.fortuna.ical4j.validate.ValidationException import java.io.Reader import java.io.StringReader import java.time.Duration import java.time.Period -import java.time.ZonedDateTime import java.time.temporal.Temporal import java.util.LinkedList import java.util.UUID @@ -124,103 +115,6 @@ open class ICalendar { // time zone helpers - /** - * Minifies a VTIMEZONE so that only these observances are kept: - * - * - the last STANDARD observance matching [start], and - * - the last DAYLIGHT observance matching [start], and - * - observances beginning after [start] - * - * Additionally, all properties other than observances and TZID are dropped. - * - * @param originalTz time zone definition to minify - * @param start start date for components (usually DTSTART); *null* if unknown - * @return minified time zone definition - */ - fun minifyVTimeZone(originalTz: VTimeZone, start: ZonedDateTime?): VTimeZone { - if (start == null) return originalTz - - var newTz: VTimeZone? = null - val keep = mutableSetOf() - - // Note: big method – maybe split? - - // find latest matching STANDARD/DAYLIGHT observances - var latestDaylight: Pair? = null - var latestStandard: Pair? = null - for (observance in originalTz.observances) { - val latest = observance.getLatestOnset(start) - - if (latest == null) // observance begins after "start", keep in any case - keep += observance - else - when (observance) { - is Standard -> - if (latestStandard == null || TemporalAdapter.isAfter(latest, latestStandard.first)) - latestStandard = Pair(latest, observance) - is Daylight -> - if (latestDaylight == null || TemporalAdapter.isAfter(latest, latestDaylight.first)) - latestDaylight = Pair(latest, observance) - } - } - - // keep latest STANDARD observance - latestStandard?.second?.let { keep += it } - - // Check latest DAYLIGHT for whether it can apply in the future. Otherwise, DST is not - // used in this time zone anymore and the DAYLIGHT component can be dropped completely. - latestDaylight?.second?.let { daylight -> - // check whether start time is in DST - if (latestStandard != null) { - val latestStandardOnset = latestStandard.second.getLatestOnset(start) - val latestDaylightOnset = daylight.getLatestOnset(start) - if (latestStandardOnset != null && latestDaylightOnset != null && latestDaylightOnset > latestStandardOnset) { - // we're currently in DST - keep += daylight - return@let - } - } - - // Observance data is using LocalDateTime. Drop time zone information for comparisons. - val startLocal = start.toLocalDateTime() - - // check RRULEs - for (rRule in daylight.getProperties>(Property.RRULE)) { - val nextDstOnset = rRule.recur.getNextDate(daylight.startDate.date, startLocal) - if (nextDstOnset != null) { - // there will be a DST onset in the future -> keep DAYLIGHT - keep += daylight - return@let - } - } - // no RRULE, check whether there's an RDATE in the future - for (rDate in daylight.getProperties>(Property.RDATE)) { - if (rDate.dates.any { !TemporalAdapter.isBefore(it, startLocal) }) { - // RDATE in the future - keep += daylight - return@let - } - } - } - - // construct minified time zone that only contains the ID and relevant observances - val relevantProperties = propertyListOf(originalTz.timeZoneId) - val relevantObservances = ComponentList(keep.toList()) - newTz = VTimeZone(relevantProperties, relevantObservances) - - // validate minified timezone - try { - newTz.validate() - } catch (e: ValidationException) { - // This should never happen! - logger.log(Level.WARNING, "Minified timezone is invalid, using original one", e) - newTz = null - } - - // use original time zone if we couldn't calculate a minified one - return newTz ?: originalTz - } - /** * Takes a string with a timezone definition and returns the time zone ID. * @param timezoneDef time zone definition (VCALENDAR with VTIMEZONE component) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt index f92251d6..c983304c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/ICalendarGenerator.kt @@ -6,24 +6,27 @@ package at.bitfire.synctools.icalendar -import at.bitfire.ical4android.ICalendar +import androidx.annotation.VisibleForTesting +import at.bitfire.vcard4android.Utils.trimToNull import net.fortuna.ical4j.data.CalendarOutputter import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.ComponentList +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.PropertyContainer +import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TemporalAdapter import net.fortuna.ical4j.model.TimeZoneRegistryFactory -import net.fortuna.ical4j.model.component.CalendarComponent import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.parameter.TzId import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.immutable.ImmutableVersion import java.io.Writer -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.ZoneId -import java.time.ZonedDateTime import java.time.temporal.Temporal +import java.util.logging.Logger import javax.annotation.WillNotClose +import kotlin.jvm.optionals.getOrNull /** * Writes an ical4j [net.fortuna.ical4j.model.Calendar] to a stream that contains an iCalendar @@ -31,6 +34,9 @@ import javax.annotation.WillNotClose */ class ICalendarGenerator { + private val logger + get() = Logger.getLogger(javaClass.name) + /** * Generates an iCalendar from the given [AssociatedComponents]. * @@ -45,16 +51,16 @@ class ICalendarGenerator { if (event.prodId != null) ical += event.prodId - // keep record of used timezones and earliest DTSTART to generate minified VTIMEZONEs + // keep record of used timezone IDs and earliest DTSTART in order to be able to add VTIMEZONEs var earliestStart: Temporal? = null - val usedTimeZones = mutableSetOf() + val usedTimezoneIds = mutableSetOf() // add main event if (event.main != null) { ical += event.main earliestStart = event.main.dtStart()?.date - usedTimeZones += timeZonesOf(event.main) + usedTimezoneIds += timeZonesOf(event.main) } // recurrence exceptions @@ -65,46 +71,91 @@ class ICalendarGenerator { if (earliestStart == null || TemporalAdapter.isBefore(start, earliestStart)) earliestStart = start } - usedTimeZones += timeZonesOf(exception) + usedTimezoneIds += timeZonesOf(exception) } - // add VTIMEZONE components + /* Add VTIMEZONE components. Unfortunately we can't generate VTIMEZONEs from the actual ZoneIds, + so we have to include the VTIMEZONEs shipped with ical4j – even if those are not the same + as the system time zones. This is a known problem, but there's currently no known solution. + Most clients ignore the VTIMEZONE anyway if they know the TZID [RFC 7809 3.1.3 "Observation + and experiments"], and Android/Java/IANA timezones are usually known to all clients. */ val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() - for (tz in usedTimeZones) { - val vTimeZone = tzReg.getTimeZone(tz.id).vTimeZone - val minifiedVTimeZone = ICalendar.minifyVTimeZone(vTimeZone, earliestStart.toZonedDateTime(tz)) + for (tzId in usedTimezoneIds) { + var vTimeZone = tzReg.getTimeZone(tzId)?.vTimeZone ?: continue + + /* Special case: sometimes, the timezone may have been loaded by an alias. + For instance, old Androids may use the "Europe/Kiev" timezone (which is then in tzId), + but ical4j returns the new "Europe/Kyiv" timezone. In that case, we want the original + name used by Android because if we would use the new TZ ID, it wouldn't be understood + by Android (and thus downgraded to the system default timezone) if we get it back + again from the server. */ + val ical4jTzId = vTimeZone.timeZoneId.value + if (ical4jTzId != tzId) { + logger.warning("Android timezone $tzId maps to ical4j $ical4jTzId. Using Android TZID.") + + /* Better not modify the VTIMEZONE because it's cached by TimeZoneRegistry, and we don't + want to modify the cache. Create a copy instead. */ + vTimeZone = copyVTimeZone(vTimeZone) + vTimeZone.replace(net.fortuna.ical4j.model.property.TzId(tzId)) + } + + // Minify VTIMEZONE and attach to iCalendar + val minifiedVTimeZone = VTimeZoneMinifier().minify(vTimeZone, earliestStart) ical += minifiedVTimeZone } CalendarOutputter(false).output(ical, to) } - private fun timeZonesOf(component: CalendarComponent): Set { - val timeZones = mutableSetOf() + /** + * Creates a one-level deep copy of the given [VTimeZone] instance. + * + * This method copies the property list and observances list from the original [VTimeZone], + * **but does not perform a deep copy of the individual properties or observances**. + * + * This allows properties and observances to be added or removed in the copied instance without affecting + * the original, but modifications to existing properties or observances will still impact the original. + * + * @param vTimeZone The [VTimeZone] instance to be copied. + * @return A new [VTimeZone] instance, safe for properties/observances to be added and removed, **but not to be modified** + */ + @VisibleForTesting + internal fun copyVTimeZone(vTimeZone: VTimeZone): VTimeZone = VTimeZone( + PropertyList(vTimeZone.propertyList.all), + ComponentList(vTimeZone.observances.toList()) + ) + + /** + * Extracts all unique time zone identifiers from the given component and its subcomponents. + * + * This method searches through all properties of the component, filtering for date properties + * that contain a TZID parameter. It also recursively processes subcomponents (such as alarms) + * if the component is a VEvent. + * + * @param component The component to extract time zone identifiers from. + * @return A set of unique time zone identifiers found in the component and its subcomponents. + */ + @VisibleForTesting + internal fun timeZonesOf(component: Component): Set { + val timeZones = mutableSetOf() - // properties + // iterate through all properties timeZones += component.propertyList.all .filterIsInstance>() - .mapNotNull { (it.date as? ZonedDateTime)?.zone } + .mapNotNull { + /* Note: When a property like DTSTART is created like DtStart(ZonedDateTime()), + the setDate() calls refreshParameters.refreshParameters() and that one sets the TZID + from the actual timezone ID. */ + it.getParameter(Parameter.TZID).getOrNull()?.value?.trimToNull() + } + .toSet() - // properties of subcomponents (alarms) + // also iterate through subcomponents like alarms recursively if (component is VEvent) for (subcomponent in component.componentList.all) - timeZones += subcomponent.propertyList.all - .filterIsInstance>() - .mapNotNull { (it.date as? ZonedDateTime)?.zone } + timeZones += timeZonesOf(subcomponent) return timeZones } - private fun Temporal?.toZonedDateTime(zoneId: ZoneId): ZonedDateTime? { - return when (this) { - is LocalDate -> this.atStartOfDay().atZone(zoneId) - is LocalDateTime -> this.atZone(zoneId) - is OffsetDateTime -> this.atZoneSameInstant(zoneId) - is Instant -> this.atZone(zoneId) - is ZonedDateTime -> this - else -> null - } - } -} \ No newline at end of file +} diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifier.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifier.kt new file mode 100644 index 00000000..e50bc4f1 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifier.kt @@ -0,0 +1,148 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.icalendar + +import net.fortuna.ical4j.model.ComponentList +import net.fortuna.ical4j.model.Property +import net.fortuna.ical4j.model.TemporalAdapter +import net.fortuna.ical4j.model.component.Daylight +import net.fortuna.ical4j.model.component.Observance +import net.fortuna.ical4j.model.component.Standard +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.property.RDate +import net.fortuna.ical4j.model.property.RRule +import net.fortuna.ical4j.validate.ValidationException +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.Temporal +import java.util.logging.Level +import java.util.logging.Logger + +class VTimeZoneMinifier { + + private val logger + get() = Logger.getLogger(VTimeZoneMinifier::class.java.name) + + /** + * Minifies a VTIMEZONE so that only these observances are kept: + * + * - the last STANDARD observance matching [startTemporal], and + * - the last DAYLIGHT observance matching [startTemporal], and + * - observances beginning after [startTemporal] + * + * Additionally, all properties other than observances and TZID are dropped. + * + * @param originalTz time zone definition to minify + * @param startTemporal start date for components (usually DTSTART); *null* if unknown + * @return minified time zone definition + */ + fun minify(originalTz: VTimeZone, startTemporal: Temporal?): VTimeZone { + // Make sure we have the earliest date available as ZonedDateTime. + if (startTemporal == null) + return originalTz + val start = asZonedDateTime( + startTemporal, + zoneId = try { + ZoneId.of(originalTz.timeZoneId.value) + } catch (_: Exception) { + ZoneId.systemDefault() + } + ) ?: return originalTz + + // list of observances that we want to keep (those at/after start) + val keep = mutableSetOf() + + // find latest matching STANDARD/DAYLIGHT observances + var latestDaylight: Pair? = null + var latestStandard: Pair? = null + for (observance in originalTz.observances) { + val latest = observance.getLatestOnset(start) + + if (latest == null) // observance begins after "start", keep in any case + keep += observance + else + when (observance) { + is Standard -> + if (latestStandard == null || TemporalAdapter.isAfter(latest, latestStandard.first)) + latestStandard = Pair(latest, observance) + is Daylight -> + if (latestDaylight == null || TemporalAdapter.isAfter(latest, latestDaylight.first)) + latestDaylight = Pair(latest, observance) + } + } + + // keep latest STANDARD observance + latestStandard?.second?.let { keep += it } + + // Check latest DAYLIGHT for whether it can apply in the future. Otherwise, DST is not + // used in this time zone anymore and the DAYLIGHT component can be dropped completely. + latestDaylight?.second?.let { daylight -> + // check whether start time is in DST + if (latestStandard != null) { + val latestStandardOnset = latestStandard.second.getLatestOnset(start) + val latestDaylightOnset = daylight.getLatestOnset(start) + if (latestStandardOnset != null && latestDaylightOnset != null && latestDaylightOnset > latestStandardOnset) { + // we're currently in DST + keep += daylight + return@let + } + } + + // Observance data is using LocalDateTime. Drop time zone information for comparisons. + val startLocal = start.toLocalDateTime() + + // check RRULEs + for (rRule in daylight.getProperties>(Property.RRULE)) { + val nextDstOnset = rRule.recur.getNextDate(daylight.startDate.date, startLocal) + if (nextDstOnset != null) { + // there will be a DST onset in the future -> keep DAYLIGHT + keep += daylight + return@let + } + } + // no RRULE, check whether there's an RDATE in the future + for (rDate in daylight.getProperties>(Property.RDATE)) { + if (rDate.dates.any { !TemporalAdapter.isBefore(it, startLocal) }) { + // RDATE in the future + keep += daylight + return@let + } + } + } + + // construct minified time zone that only contains the ID and relevant observances + val relevantProperties = propertyListOf(originalTz.timeZoneId) + val relevantObservances = ComponentList(keep.toList()) + val newTz = VTimeZone(relevantProperties, relevantObservances) + + // validate minified timezone + try { + newTz.validate() + } catch (e: ValidationException) { + // This should never happen! + logger.log(Level.WARNING, "Minified timezone is invalid, using original one", e) + } + + // use original time zone if we couldn't calculate a minified one + return newTz + } + + private fun asZonedDateTime(temporal: Temporal, zoneId: ZoneId = ZoneId.systemDefault()): ZonedDateTime? = + when (temporal) { + is LocalDate -> temporal.atStartOfDay().atZone(zoneId) + is LocalDateTime -> temporal.atZone(zoneId) + is OffsetDateTime -> temporal.atZoneSameInstant(zoneId) + is Instant -> temporal.atZone(zoneId) + is ZonedDateTime -> temporal + else -> null + } + +} \ No newline at end of file diff --git a/lib/src/main/resources/ical4j.properties b/lib/src/main/resources/ical4j.properties index edc3d429..b0d23618 100644 --- a/lib/src/main/resources/ical4j.properties +++ b/lib/src/main/resources/ical4j.properties @@ -1,6 +1,5 @@ net.fortuna.ical4j.timezone.cache.impl=net.fortuna.ical4j.util.MapTimeZoneCache net.fortuna.ical4j.timezone.offset.negative_dst_supported=true -net.fortuna.ical4j.timezone.registry=at.bitfire.ical4android.AndroidCompatTimeZoneRegistry$Factory net.fortuna.ical4j.timezone.update.enabled=false ical4j.unfolding.relaxed=true ical4j.parsing.relaxed=true diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt index 6ea598bf..35f3a7cf 100644 --- a/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt +++ b/lib/src/test/kotlin/at/bitfire/ical4android/ICalendarTest.kt @@ -6,14 +6,9 @@ package at.bitfire.ical4android -import net.fortuna.ical4j.data.CalendarBuilder -import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.Property.TRIGGER -import net.fortuna.ical4j.model.TimeZone -import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VAlarm -import net.fortuna.ical4j.model.component.VTimeZone import net.fortuna.ical4j.model.parameter.Related import net.fortuna.ical4j.model.property.Color import net.fortuna.ical4j.model.property.DtEnd @@ -21,13 +16,11 @@ import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.Due import net.fortuna.ical4j.model.property.Duration import net.fortuna.ical4j.model.property.Trigger -import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Test import java.io.StringReader -import java.time.LocalDateTime import java.time.Period import java.time.ZonedDateTime import java.time.temporal.Temporal @@ -35,33 +28,10 @@ import kotlin.jvm.optionals.getOrNull class ICalendarTest { - // UTC timezone - private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - val tzUTC = tzRegistry.getTimeZone(TimeZones.UTC_ID)!! - private val vtzUTC = tzUTC.vTimeZone - - // Austria (Europa/Vienna) uses DST regularly - private val vtzVienna = readTimeZone("Vienna.ics") - - // Pakistan (Asia/Karachi) used DST only in 2002, 2008 and 2009; no known future occurrences - private val vtzKarachi = readTimeZone("Karachi.ics") - - // Somalia (Africa/Mogadishu) has never used DST - private val vtzMogadishu = readTimeZone("Mogadishu.ics") - // current time stamp private val currentTime = ZonedDateTime.now() - private fun readTimeZone(fileName: String): VTimeZone { - javaClass.classLoader!!.getResourceAsStream("tz/$fileName").use { tzStream -> - val cal = CalendarBuilder().build(tzStream) - val vTimeZone = cal.getComponent(Component.VTIMEZONE).get() - return vTimeZone - } - } - - @Test fun testFromReader_calendarProperties() { val calendar = ICalendar.fromReader( @@ -103,89 +73,6 @@ class ICalendarTest { } - @Test - fun testMinifyVTimezone_UTC() { - // Keep the only observance for UTC. - // DATE-TIME values in UTC are usually noted with ...Z and don't have a VTIMEZONE, - // but it is allowed to write them as TZID=Etc/UTC. - assertEquals(1, vtzUTC.observances.size) - - val minified = ICalendar.minifyVTimeZone(vtzUTC, vtzUTC.zonedDateTime("2020-06-12T00:00")) - - assertEquals(1, minified.observances.size) - } - - @Test - fun testMinifyVTimezone_removeObsoleteDstObservances() { - // Remove obsolete observances when DST is used. - assertEquals(6, vtzVienna.observances.size) - // By default, the earliest observance is in 1893. We can drop that for events in 2020. - assertEquals(LocalDateTime.parse("1893-04-01T00:00:00"), vtzVienna.observances.minOfOrNull { it.startDate.date }) - - val minified = ICalendar.minifyVTimeZone(vtzVienna, vtzVienna.zonedDateTime("2020-01-01")) - - assertEquals(2, minified.observances.size) - // now earliest observance for STANDARD/DAYLIGHT is 1996/1981 - assertEquals(LocalDateTime.parse("1996-10-27T03:00:00"), minified.observances[0].startDate.date) - assertEquals(LocalDateTime.parse("1981-03-29T02:00:00"), minified.observances[1].startDate.date) - } - - @Test - fun testMinifyVTimezone_removeObsoleteObservances() { - // Remove obsolete observances when DST is not used. Mogadishu had several time zone changes, - // but now there is a simple offest without DST. - assertEquals(4, vtzMogadishu.observances.size) - - val minified = ICalendar.minifyVTimeZone(vtzMogadishu, vtzMogadishu.zonedDateTime("1961-10-01")) - - assertEquals(1, minified.observances.size) - } - - @Test - fun testMinifyVTimezone_keepFutureObservances() { - // Keep future observances. - ICalendar.minifyVTimeZone(vtzVienna, vtzVienna.zonedDateTime("1975-10-01")).let { minified -> - val sortedStartDates = minified.observances - .map { it.startDate.date } - .sorted() - .map { it.toString() } - - assertEquals( - listOf("1916-04-30T23:00", "1916-10-01T01:00", "1981-03-29T02:00", "1996-10-27T03:00"), - sortedStartDates - ) - } - - ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("1961-10-01")).let { minified -> - assertEquals(4, minified.observances.size) - } - - ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("1975-10-01")).let { minified -> - assertEquals(3, minified.observances.size) - } - - ICalendar.minifyVTimeZone(vtzMogadishu, vtzMogadishu.zonedDateTime("1931-10-01")).let { minified -> - assertEquals(3, minified.observances.size) - } - } - - @Test - fun testMinifyVTimezone_keepDstWhenStartInDst() { - // Keep DST when there are no obsolete observances, but start time is in DST. - ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("2009-10-31")).let { minified -> - assertEquals(2, minified.observances.size) - } - } - - @Test - fun testMinifyVTimezone_removeDstWhenNotUsedAnymore() { - // Remove obsolete observances (including DST) when DST is not used anymore. - ICalendar.minifyVTimeZone(vtzKarachi, vtzKarachi.zonedDateTime("2010-01-01")).let { minified -> - assertEquals(1, minified.observances.size) - } - } - - @Test fun testTimezoneDefToTzId_Valid() { assertEquals( @@ -367,9 +254,4 @@ class ICalendarTest { assertEquals(8*24*60, min) }*/ -} - -private fun VTimeZone.zonedDateTime(text: String): ZonedDateTime { - val dateTimeText = if ('T' in text) text else "${text}T00:00:00" - return LocalDateTime.parse(dateTimeText).atZone(TimeZone(this).toZoneId()) } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jConfigurationTest.kt b/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jConfigurationTest.kt deleted file mode 100644 index e9d547c1..00000000 --- a/lib/src/test/kotlin/at/bitfire/ical4android/Ical4jConfigurationTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This file is part of bitfireAT/synctools which is released under GPLv3. - * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package at.bitfire.ical4android - -import net.fortuna.ical4j.model.TimeZoneRegistryFactory -import org.junit.Assert -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue - -import org.junit.Test - -class Ical4jConfigurationTest { - - @Test - fun testTimeZoneRegistryFactoryConfigured() { - val registry = TimeZoneRegistryFactory.getInstance().createRegistry() - assertTrue(registry is AndroidCompatTimeZoneRegistry) - } - -} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt index 93a33373..56be7778 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/ICalendarGeneratorTest.kt @@ -16,13 +16,17 @@ import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RecurrenceId +import net.fortuna.ical4j.model.property.TzId import net.fortuna.ical4j.model.property.Uid import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertTrue import org.junit.Test import java.io.StringWriter import java.time.Duration import java.time.Instant import java.time.LocalDateTime +import java.time.ZoneId import java.time.ZonedDateTime import java.time.temporal.Temporal @@ -122,4 +126,126 @@ class ICalendarGeneratorTest { "END:VCALENDAR\r\n", iCal.toString()) } + @Test + fun `Write event that uses old Kiev timezone`() { + // Test the special case where Android uses "Europe/Kiev" but ical4j uses "Europe/Kyiv". + // The output should preserve the original Android timezone name. + + // We will provide Europe/Kiev for ICalendarGenerator + val tzKiev = ZoneId.of("Europe/Kiev") + assertEquals("Europe/Kiev", tzKiev.id) + + // Verify that ical4j returns a VTIMEZONE with the new Europe/Kyiv TZID (by alias) + val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() + // We call getTimeZone(Europe/Kiev), but we get VTIMEZONE(Europe/Kyiv): + assertEquals("Europe/Kyiv", tzReg.getTimeZone(tzKiev.id).id) + + // Generate the iCalendar (must NOT map Europe/Kiev to Europe/Kyiv silently) + val iCal = StringWriter() + writer.write(AssociatedEvents( + main = VEvent(propertyListOf( + Uid("KIEVTEST"), + DtStart(ZonedDateTime.of(LocalDateTime.parse("2023-01-01T12:00:00"), tzKiev)), + DtEnd(ZonedDateTime.of(LocalDateTime.parse("2023-01-01T14:00:00"), tzKiev)), + DtStamp("20230101T120000Z") + )), + prodId = userAgent, + exceptions = listOf() + ), iCal) + + // Check TZID of generated VTIMEZONE (must match original timezone ID) + val pattern = Regex( + "BEGIN:VCALENDAR\r\n" + + "VERSION:2.0\r\n" + + "PRODID:TestUA/1.0\r\n" + + "BEGIN:VEVENT\r\n" + + "UID:KIEVTEST\r\n" + + "DTSTART;TZID=Europe/Kiev:20230101T120000\r\n" + + "DTEND;TZID=Europe/Kiev:20230101T140000\r\n" + + "DTSTAMP:20230101T120000Z\r\n" + + "END:VEVENT\r\n" + + "BEGIN:VTIMEZONE\r\n" + + ".*TZID:Europe/Kiev\r\n" + + ".*END:VTIMEZONE", + setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL) + ) + assertTrue(iCal.toString().contains(pattern)) + } + + + @Test + fun `copyVTimeZone result properties can be added without modifying original`() { + // Get a timezone from the registry + val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() + val originalVTimeZone = tzReg.getTimeZone("Europe/Berlin").vTimeZone + val originalVTZ = originalVTimeZone.toString() + + // Create a copy using the method + val copiedVTimeZone = writer.copyVTimeZone(originalVTimeZone) + + // Verify that the copy uses new lists + assertEquals(originalVTimeZone.propertyList, copiedVTimeZone.propertyList) + assertNotSame(originalVTimeZone.propertyList, copiedVTimeZone.propertyList) + assertEquals(originalVTimeZone.observances, copiedVTimeZone.observances) + assertNotSame(originalVTimeZone.observances, copiedVTimeZone.observances) + + // Remove/add properties from/to the copy and ensure the original is not affected + copiedVTimeZone.propertyList.replace(TzId("Something/Else")) + + // This would still modify the original, causing the cache to be corrupted and the test to fail: + // copiedVTimeZone.timeZoneId.value = "Something/Else" + + // Verify original timezone is unmodified by checking string representation + assertEquals(originalVTZ, originalVTimeZone.toString()) + } + + + @Test + fun `timeZonesOf extracts TZIDs from date properties`() { + val tzBerlin = ZoneId.of("Europe/Berlin") + val tzLondon = ZoneId.of("Europe/London") + + val component = VEvent(propertyListOf( + DtStart(ZonedDateTime.of(LocalDateTime.parse("2019-01-01T10:00:00"), tzBerlin)), + DtEnd(ZonedDateTime.of(LocalDateTime.parse("2019-01-01T12:00:00"), tzLondon)) + )) + + val result = writer.timeZonesOf(component) + assertEquals(setOf("Europe/Berlin", "Europe/London"), result) + } + + @Test + fun `timeZonesOf returns empty set when no TZIDs present`() { + val component = VEvent(propertyListOf( + DtStart(Instant.parse("2019-01-01T10:00:00Z")), + DtEnd(Instant.parse("2019-01-01T12:00:00Z")) + )) + + val result = writer.timeZonesOf(component) + assertTrue(result.isEmpty()) + } + + @Test + fun `timeZonesOf extracts TZIDs from subcomponents`() { + val tzBerlin = ZoneId.of("Europe/Berlin") + val tzLondon = ZoneId.of("Europe/London") + + val component = VEvent(propertyListOf( + DtStart(ZonedDateTime.of(LocalDateTime.parse("2019-01-01T10:00:00"), tzBerlin)) + ), ComponentList(listOf( + VAlarm(propertyListOf( + DtStart(ZonedDateTime.of(LocalDateTime.parse("2019-01-01T09:00:00"), tzLondon)) + )) + ))) + + val result = writer.timeZonesOf(component) + assertEquals(setOf("Europe/Berlin", "Europe/London"), result) + } + + @Test + fun `timeZonesOf returns empty set for empty component`() { + val result = writer.timeZonesOf(VEvent()) + assertTrue(result.isEmpty()) + } + } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifierTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifierTest.kt new file mode 100644 index 00000000..9b7f54ae --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/icalendar/VTimeZoneMinifierTest.kt @@ -0,0 +1,136 @@ +/* + * This file is part of bitfireAT/synctools which is released under GPLv3. + * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package at.bitfire.synctools.icalendar + +import net.fortuna.ical4j.data.CalendarBuilder +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.util.TimeZones +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime + +class VTimeZoneMinifierTest { + + private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() + val tzUTC = tzRegistry.getTimeZone(TimeZones.UTC_ID)!! + + private val vtzUTC = tzUTC.vTimeZone + + // Austria (Europa/Vienna) uses DST regularly + private val vtzVienna = readTimeZone("Vienna.ics") + + // Pakistan (Asia/Karachi) used DST only in 2002, 2008 and 2009; no known future occurrences + private val vtzKarachi = readTimeZone("Karachi.ics") + + // Somalia (Africa/Mogadishu) has never used DST + private val vtzMogadishu = readTimeZone("Mogadishu.ics") + + private val minifier = VTimeZoneMinifier() + + + @Test + fun testMinifyTimezone_UTC() { + // Keep the only observance for UTC. + // DATE-TIME values in UTC are usually noted with ...Z and don't have a VTIMEZONE, + // but it is allowed to write them as TZID=Etc/UTC. + assertEquals(1, vtzUTC.observances.size) + + val minified = minifier.minify(vtzUTC, vtzUTC.zonedDateTime("2020-06-12T00:00")) + + assertEquals(1, minified.observances.size) + } + + @Test + fun testMinifyTimezone_removeObsoleteDstObservances() { + // Remove obsolete observances when DST is used. + assertEquals(6, vtzVienna.observances.size) + // By default, the earliest observance is in 1893. We can drop that for events in 2020. + assertEquals(LocalDateTime.parse("1893-04-01T00:00:00"), vtzVienna.observances.minOfOrNull { it.startDate.date }) + + val minified = minifier.minify(vtzVienna, vtzVienna.zonedDateTime("2020-01-01")) + + assertEquals(2, minified.observances.size) + // now earliest observance for STANDARD/DAYLIGHT is 1996/1981 + assertEquals(LocalDateTime.parse("1996-10-27T03:00:00"), minified.observances[0].startDate.date) + assertEquals(LocalDateTime.parse("1981-03-29T02:00:00"), minified.observances[1].startDate.date) + } + + @Test + fun testMinifyTimezone_removeObsoleteObservances() { + // Remove obsolete observances when DST is not used. Mogadishu had several time zone changes, + // but now there is a simple offset without DST. + assertEquals(4, vtzMogadishu.observances.size) + + val minified = minifier.minify(vtzMogadishu, vtzMogadishu.zonedDateTime("1961-10-01")) + + assertEquals(1, minified.observances.size) + } + + @Test + fun testMinifyTimezone_keepFutureObservances() { + // Keep future observances. + minifier.minify(vtzVienna, vtzVienna.zonedDateTime("1975-10-01")).let { minified -> + val sortedStartDates = minified.observances + .map { it.startDate.date } + .sorted() + .map { it.toString() } + + assertEquals( + listOf("1916-04-30T23:00", "1916-10-01T01:00", "1981-03-29T02:00", "1996-10-27T03:00"), + sortedStartDates + ) + } + + minifier.minify(vtzKarachi, vtzKarachi.zonedDateTime("1961-10-01")).let { minified -> + assertEquals(4, minified.observances.size) + } + + minifier.minify(vtzKarachi, vtzKarachi.zonedDateTime("1975-10-01")).let { minified -> + assertEquals(3, minified.observances.size) + } + + minifier.minify(vtzMogadishu, vtzMogadishu.zonedDateTime("1931-10-01")).let { minified -> + assertEquals(3, minified.observances.size) + } + } + + @Test + fun testMinifyTimezone_keepDstWhenStartInDst() { + // Keep DST when there are no obsolete observances, but start time is in DST. + minifier.minify(vtzKarachi, vtzKarachi.zonedDateTime("2009-10-31")).let { minified -> + assertEquals(2, minified.observances.size) + } + } + + @Test + fun testMinifyTimezone_removeDstWhenNotUsedAnymore() { + // Remove obsolete observances (including DST) when DST is not used anymore. + minifier.minify(vtzKarachi, vtzKarachi.zonedDateTime("2010-01-01")).let { minified -> + assertEquals(1, minified.observances.size) + } + } + + + private fun readTimeZone(fileName: String): VTimeZone { + javaClass.classLoader!!.getResourceAsStream("tz/$fileName").use { tzStream -> + val cal = CalendarBuilder().build(tzStream) + val vTimeZone = cal.getComponent(Component.VTIMEZONE).get() + return vTimeZone + } + } + + private fun VTimeZone.zonedDateTime(dateTimeStr: String): ZonedDateTime { + val dateTimeText = if ('T' in dateTimeStr) dateTimeStr else "${dateTimeStr}T00:00:00" + val zoneId = ZoneId.of(timeZoneId.value) + return ZonedDateTime.of(LocalDateTime.parse(dateTimeText), zoneId) + } + +} \ No newline at end of file From 3fe02cc3a21bf0bd8d1eec03001809a3f1c4435b Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 10:36:14 +0100 Subject: [PATCH 15/24] Update UidHandler and tests --- .../synctools/mapping/calendar/handler/UidHandler.kt | 6 +++--- .../synctools/mapping/calendar/handler/UidHandlerTest.kt | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt index 0c2d358a..8f79db2b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandler.kt @@ -8,6 +8,7 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events +import at.bitfire.synctools.icalendar.plusAssign import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Uid @@ -17,9 +18,8 @@ class UidHandler: AndroidEventFieldHandler { // Should always be available because AndroidEventHandler ensures there's a UID to be RFC 5545-compliant. // However technically it can be null (and no UID is OK according to RFC 2445). val uid = main.entityValues.getAsString(Events.UID_2445) - TODO("ical4j 4.x") - /*if (uid != null) - to.properties += Uid(uid)*/ + if (uid != null) + to += Uid(uid) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt index f3ba6816..37818c95 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/UidHandlerTest.kt @@ -13,12 +13,11 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.jvm.optionals.getOrNull -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class UidHandlerTest { @@ -29,7 +28,7 @@ class UidHandlerTest { val result = VEvent() val entity = Entity(ContentValues()) handler.process(entity, entity, result) - assertNull(result.uid) + assertNull(result.uid.getOrNull()) } @Test @@ -39,8 +38,7 @@ class UidHandlerTest { )) val result = VEvent() handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals("from-event", result.uid.value) + assertEquals("from-event", result.uid?.getOrNull()?.value) } } \ No newline at end of file From 658e0cd7d8b6e5182dd0faf0893824cb2009ef24 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 11:19:12 +0100 Subject: [PATCH 16/24] Update OriginalInstanceTimeHandler and tests --- .../mapping/calendar/AndroidEventHandler.kt | 4 +- .../handler/OriginalInstanceTimeHandler.kt | 51 ++++++++----------- .../OriginalInstanceTimeHandlerTest.kt | 27 ++++------ 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt index 389bda99..2b0e481b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt @@ -34,11 +34,9 @@ import at.bitfire.synctools.mapping.calendar.handler.UnknownPropertiesHandler import at.bitfire.synctools.mapping.calendar.handler.UrlHandler import at.bitfire.synctools.storage.calendar.EventAndExceptions import at.bitfire.synctools.storage.calendar.EventsContract -import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent -import net.fortuna.ical4j.model.parameter.Value import net.fortuna.ical4j.model.property.ExDate import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.RDate @@ -63,7 +61,7 @@ class AndroidEventHandler( private val fieldHandlers: Array = arrayOf( // event row fields UidHandler(), - OriginalInstanceTimeHandler(tzRegistry), + OriginalInstanceTimeHandler(), TitleHandler(), LocationHandler(), StartTimeHandler(tzRegistry), diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt index 5373a303..4885a931 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandler.kt @@ -8,17 +8,19 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events -import at.bitfire.ical4android.util.DateUtils -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.TimeZoneRegistry +import at.bitfire.synctools.icalendar.DatePropertyTzMapper +import at.bitfire.synctools.icalendar.plusAssign +import net.fortuna.ical4j.model.Parameter +import net.fortuna.ical4j.model.ParameterList import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.parameter.TzId import net.fortuna.ical4j.model.property.RecurrenceId -import net.fortuna.ical4j.util.TimeZones +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId -class OriginalInstanceTimeHandler( - private val tzRegistry: TimeZoneRegistry -): AndroidEventFieldHandler { +class OriginalInstanceTimeHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { // only applicable to exceptions, not to main events @@ -26,30 +28,21 @@ class OriginalInstanceTimeHandler( return val values = from.entityValues - TODO("ical4j 4.x") - /*values.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime -> + values.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime -> val originalAllDay = (values.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0 - val originalDate = - if (originalAllDay) - Date(originalInstanceTime) - else - DateTime(originalInstanceTime) - - if (originalDate is DateTime) { + val instant = Instant.ofEpochMilli(originalInstanceTime) + to += if (originalAllDay) { + RecurrenceId(LocalDate.ofInstant(instant, ZoneId.systemDefault())) + } else { // get DTSTART time zone - val startTzId = DateUtils.findAndroidTimezoneID(values.getAsString(Events.EVENT_TIMEZONE)) - val startTz = tzRegistry.getTimeZone(startTzId) - - if (startTz != null) { - if (TimeZones.isUtc(startTz)) - originalDate.isUtc = true - else - originalDate.timeZone = startTz - } + val startTzId = DatePropertyTzMapper.systemTzId(values.getAsString(Events.EVENT_TIMEZONE)) + val zoneId = startTzId?.let { ZoneId.of(startTzId) } ?: ZoneId.systemDefault() + RecurrenceId( + ParameterList(mutableListOf(TzId(startTzId))), + LocalDateTime.ofInstant(instant, zoneId) + ) } - - to.properties += RecurrenceId(originalDate) - }*/ + } } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt index 7c14b022..7f85828c 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/OriginalInstanceTimeHandlerTest.kt @@ -10,31 +10,25 @@ import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import at.bitfire.synctools.icalendar.recurrenceId import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.RecurrenceId import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class OriginalInstanceTimeHandlerTest { - private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna")!! + private val tzVienna = ZoneId.of("Europe/Vienna") - private val handler = OriginalInstanceTimeHandler(tzRegistry) + private val handler = OriginalInstanceTimeHandler() - init { - TODO("ical4j 4.x") - } - - /*@Test + @Test fun `Original event is all-day`() { val result = VEvent() val entity = Entity(contentValuesOf( @@ -42,7 +36,7 @@ class OriginalInstanceTimeHandlerTest { Events.ORIGINAL_ALL_DAY to 1 )) handler.process(entity, Entity(ContentValues()), result) - assertEquals(RecurrenceId(Date("20200707")), result.recurrenceId) + assertEquals(RecurrenceId(LocalDate.of(2020, 7, 7)), result.recurrenceId) } @Test @@ -54,7 +48,8 @@ class OriginalInstanceTimeHandlerTest { Events.EVENT_TIMEZONE to tzVienna.id )) handler.process(entity, Entity(ContentValues()), result) - assertEquals(RecurrenceId(DateTime("20250922T161348", tzVienna)), result.recurrenceId) - }*/ + val viennaDateTime = ZonedDateTime.of(2025, 9, 22, 16, 13, 48, 0, tzVienna) + assertEquals(RecurrenceId(viennaDateTime), result.recurrenceId) + } } \ No newline at end of file From 8a6dd5b256c29f3aac5f7587bfeacd32e6121db3 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 11:20:30 +0100 Subject: [PATCH 17/24] Update TitleHandler and tests --- .../synctools/mapping/calendar/handler/TitleHandler.kt | 6 +++--- .../synctools/mapping/calendar/handler/TitleHandlerTest.kt | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt index 1b26c592..546d56f5 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandler.kt @@ -8,6 +8,7 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.vcard4android.Utils.trimToNull import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Summary @@ -16,9 +17,8 @@ class TitleHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val summary = from.entityValues.getAsString(Events.TITLE).trimToNull() - TODO("ical4j 4.x") - /*if (summary != null) - to.properties += Summary(summary)*/ + if (summary != null) + to += Summary(summary) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt index fcc5654e..8477cc76 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/TitleHandlerTest.kt @@ -13,12 +13,10 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class TitleHandlerTest { From 4ad56392da9bbdf81b9cf94b57412ec047a46dd4 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 11:21:14 +0100 Subject: [PATCH 18/24] Update LocationHandler and tests --- .../synctools/mapping/calendar/handler/LocationHandler.kt | 6 +++--- .../mapping/calendar/handler/LocationHandlerTest.kt | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt index 51c429e2..7645d42b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandler.kt @@ -8,6 +8,7 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.vcard4android.Utils.trimToNull import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Location @@ -16,9 +17,8 @@ class LocationHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val location = from.entityValues.getAsString(Events.EVENT_LOCATION).trimToNull() - TODO("ical4j 4.x") - /*if (location != null) - to.properties += Location(location)*/ + if (location != null) + to += Location(location) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt index a7dd435d..547233d6 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/LocationHandlerTest.kt @@ -13,12 +13,10 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class LocationHandlerTest { From 45ee957d8987807cdf1b1429ef610ae686ac0c39 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 11:40:43 +0100 Subject: [PATCH 19/24] Update StartTimeHandler and tests --- .../mapping/calendar/AndroidEventHandler.kt | 2 +- .../calendar/handler/AndroidTimeField.kt | 33 +++++++++++++++++-- .../calendar/handler/StartTimeHandler.kt | 14 +++----- .../calendar/handler/StartTimeHandlerTest.kt | 22 ++++++------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt index 2b0e481b..79f5661c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt @@ -64,7 +64,7 @@ class AndroidEventHandler( OriginalInstanceTimeHandler(), TitleHandler(), LocationHandler(), - StartTimeHandler(tzRegistry), + StartTimeHandler(), EndTimeHandler(tzRegistry), DurationHandler(tzRegistry), RecurrenceFieldsHandler(tzRegistry), diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeField.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeField.kt index 762792c1..e9a509f0 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeField.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AndroidTimeField.kt @@ -11,7 +11,12 @@ import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TimeZoneRegistry import net.fortuna.ical4j.util.TimeZones +import java.time.Instant +import java.time.LocalDate import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.temporal.Temporal /** * Converts timestamps from the [android.provider.CalendarContract.Events.DTSTART] or [android.provider.CalendarContract.Events.DTEND] @@ -25,17 +30,41 @@ class AndroidTimeField( private val timestamp: Long, private val timeZone: String?, private val allDay: Boolean, - private val tzRegistry: TimeZoneRegistry + private val tzRegistry: TimeZoneRegistry? = null ) { /** ID of system default timezone */ private val defaultTzId by lazy { ZoneId.systemDefault().id } + /** + * Converts the given Android date/time into java time temporal object. + * + * @return `Loca` in case of an all-day event, `DateTime` in case of a non-all-day event + */ + fun asTemporal(): Temporal { + val instant = Instant.ofEpochMilli(timestamp) + + if (allDay) + return LocalDate.ofInstant(instant, ZoneId.systemDefault()) + + // non-all-day + val tzId = timeZone + ?: ZoneId.systemDefault().id // safe fallback (should never be used/needed because the calendar provider requires EVENT_TIMEZONE) + + val timezone = if (tzId == AndroidTimeUtils.TZID_UTC || tzId == TimeZones.UTC_ID || tzId == TimeZones.IBM_UTC_ID) + ZoneOffset.UTC + else + ZoneId.of(tzId) + + return ZonedDateTime.ofInstant(instant, timezone) + } + /** * Converts the given Android date/time into an ical4j date property. * * @return `Date` in case of an all-day event, `DateTime` in case of a non-all-day event */ + @Deprecated("Use asTemporal() instead.") fun asIcal4jDate(): Date { if (allDay) return Date(timestamp) @@ -54,7 +83,7 @@ class AndroidTimeField( val timezone = if (tzId == AndroidTimeUtils.TZID_UTC || tzId == TimeZones.UTC_ID || tzId == TimeZones.IBM_UTC_ID) null // indicates UTC else - (tzRegistry.getTimeZone(tzId) ?: tzRegistry.getTimeZone(defaultTzId)) + (tzRegistry?.getTimeZone(tzId) ?: tzRegistry?.getTimeZone(defaultTzId)) return DateTime(timestamp).also { dateTime -> if (timezone == null) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt index e97c75e4..6db42561 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandler.kt @@ -9,13 +9,11 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.synctools.exception.InvalidLocalResourceException -import net.fortuna.ical4j.model.TimeZoneRegistry +import at.bitfire.synctools.icalendar.plusAssign import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart -class StartTimeHandler( - private val tzRegistry: TimeZoneRegistry -): AndroidEventFieldHandler { +class StartTimeHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val values = from.entityValues @@ -25,12 +23,10 @@ class StartTimeHandler( val start = AndroidTimeField( timestamp = values.getAsLong(Events.DTSTART) ?: throw InvalidLocalResourceException("Missing DTSTART"), timeZone = values.getAsString(Events.EVENT_TIMEZONE), - allDay = allDay, - tzRegistry = tzRegistry - ).asIcal4jDate() + allDay = allDay + ).asTemporal() - TODO("ical4j 4.x") - //to.properties += DtStart(start) + to += DtStart(start) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt index 90d50878..3a88a783 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/StartTimeHandlerTest.kt @@ -11,26 +11,24 @@ import android.content.Entity import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf import at.bitfire.synctools.exception.InvalidLocalResourceException +import at.bitfire.synctools.icalendar.dtStart import at.bitfire.synctools.util.AndroidTimeUtils import junit.framework.TestCase.assertEquals -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtStart -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class StartTimeHandlerTest { - private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna")!! + private val tzVienna = ZoneId.of("Europe/Vienna") - private val handler = StartTimeHandler(tzRegistry) + private val handler = StartTimeHandler() @Test fun `All-day event`() { @@ -41,8 +39,8 @@ class StartTimeHandlerTest { Events.EVENT_TIMEZONE to AndroidTimeUtils.TZID_UTC )) handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals(DtStart(Date("20200621")), result.startDate) + val localDate = LocalDate.of(2020, 6, 21) + assertEquals(DtStart(localDate), result.dtStart()) } @Test @@ -53,8 +51,8 @@ class StartTimeHandlerTest { Events.EVENT_TIMEZONE to "Europe/Vienna" )) handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals(DtStart(DateTime("20200621T120000", tzVienna)), result.startDate) + val viennaDateTime = ZonedDateTime.of(2020, 6, 21, 12, 0, 0, 0, tzVienna) + assertEquals(DtStart(viennaDateTime), result.dtStart()) } @Test(expected = InvalidLocalResourceException::class) From ec52d85293ec6cbc8af2fb2c146923237ed38ffa Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 13:37:09 +0100 Subject: [PATCH 20/24] Update EndTimeHandler and tests --- .../synctools/icalendar/Ical4jHelpers.kt | 5 ++ .../mapping/calendar/AndroidEventHandler.kt | 2 +- .../calendar/handler/EndTimeHandler.kt | 18 ++----- .../calendar/handler/EndTimeHandlerTest.kt | 54 +++++++++++-------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt index 3a79d573..554a7f5b 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt @@ -16,6 +16,7 @@ import net.fortuna.ical4j.model.PropertyContainer import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.component.CalendarComponent import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RecurrenceId import net.fortuna.ical4j.model.property.Sequence @@ -51,6 +52,10 @@ fun CalendarComponent.dtStart(): DtStart? { return getProperty>(Property.DTSTART).getOrNull() } +fun CalendarComponent.dtEnd(): DtEnd? { + return getProperty>(Property.DTEND).getOrNull() +} + fun VEvent.requireDtStart(): DtStart = getProperty>(Property.DTSTART).getOrNull() ?: throw InvalidICalendarException("Missing DTSTART in VEVENT") diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt index 79f5661c..98bbfc82 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt @@ -65,7 +65,7 @@ class AndroidEventHandler( TitleHandler(), LocationHandler(), StartTimeHandler(), - EndTimeHandler(tzRegistry), + EndTimeHandler(), DurationHandler(tzRegistry), RecurrenceFieldsHandler(tzRegistry), DescriptionHandler(), diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt index 01382478..2ce5a11c 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandler.kt @@ -9,12 +9,11 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events import androidx.annotation.VisibleForTesting -import net.fortuna.ical4j.model.TimeZoneRegistry +import at.bitfire.synctools.icalendar.plusAssign import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtEnd import java.time.Duration import java.time.Instant -import java.util.logging.Logger /** * Maps a potentially present [Events.DTEND] to a VEvent [DtEnd] property. @@ -24,12 +23,7 @@ import java.util.logging.Logger * - If [Events.DURATION] is present / not null, [DurationHandler] is responsible for generating the VEvent's [DtEnd]. * - If [Events.DURATION] is null / not present, this class is responsible for generating the VEvent's [DtEnd]. */ -class EndTimeHandler( - private val tzRegistry: TimeZoneRegistry -): AndroidEventFieldHandler { - - private val logger - get() = Logger.getLogger(javaClass.name) +class EndTimeHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val values = from.entityValues @@ -56,12 +50,10 @@ class EndTimeHandler( timestamp = tsEnd, timeZone = values.getAsString(Events.EVENT_END_TIMEZONE) ?: values.getAsString(Events.EVENT_TIMEZONE), // if end timezone is not present, assume same as for start - allDay = allDay, - tzRegistry = tzRegistry - ).asIcal4jDate() + allDay = allDay + ).asTemporal() - TODO("ical4j 4.x") - //to.properties += DtEnd(end) + to += DtEnd(end) } @VisibleForTesting diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt index 89ac6a8f..83e695d6 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/EndTimeHandlerTest.kt @@ -9,39 +9,39 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.util.DateUtils.toEpochMilli +import at.bitfire.synctools.icalendar.dtEnd import junit.framework.TestCase.assertEquals -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.TimeZoneRegistryFactory +import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.parameter.TzId import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.util.TimeZones import org.junit.Assert.assertNull import org.junit.Assume -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.time.LocalDate +import java.time.LocalDateTime import java.time.OffsetDateTime import java.time.ZoneId import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.temporal.Temporal +import kotlin.jvm.optionals.getOrNull -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class EndTimeHandlerTest { - private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna")!! + private val tzVienna = ZoneId.of("Europe/Vienna")!! - private val handler = EndTimeHandler(tzRegistry) + private val handler = EndTimeHandler() // Note: When the calendar provider sets a non-null DTEND, it implies that the event is not recurring. - init { - TODO("ical4j 4.x") - } - /*@Test + @Test fun `All-day event`() { val result = VEvent() val entity = Entity(contentValuesOf( @@ -50,7 +50,8 @@ class EndTimeHandlerTest { Events.DTEND to 1592697600000L, // 21/06/2020 )) handler.process(entity, entity, result) - assertEquals(DtEnd(Date("20200621")), result.endDate) + val localDate = LocalDate.of(2020, 6, 21) + assertEquals(DtEnd(localDate), result.dtEnd()) } @Test @@ -61,7 +62,8 @@ class EndTimeHandlerTest { Events.DTSTART to 1592697600000L // 21/06/2020; DTSTART is required for DTEND to be processed )) handler.process(entity, entity, result) - assertEquals(DtEnd(Date("20200622")), result.endDate) + val localDate = LocalDate.of(2020, 6, 22) + assertEquals(DtEnd(localDate), result.dtEnd()) } @Test @@ -75,7 +77,8 @@ class EndTimeHandlerTest { Events.EVENT_END_TIMEZONE to "Europe/Vienna" )) handler.process(entity, entity, result) - assertEquals(DtEnd(DateTime("20200621T120000", tzVienna)), result.endDate) + val viennaDateTime = ZonedDateTime.of(2020, 6, 21, 12, 0, 0, 0, tzVienna) + assertEquals(DtEnd(viennaDateTime), result.dtEnd()) } @Test @@ -88,12 +91,13 @@ class EndTimeHandlerTest { Events.DTEND to 1592733600000L // 21/06/2020 12:00 +0200 )) handler.process(entity, entity, result) - assertEquals(DtEnd(DateTime("20200621T120000", tzVienna)), result.endDate) + val viennaDateTime = ZonedDateTime.of(2020, 6, 21, 12, 0, 0, 0, tzVienna) + assertEquals(DtEnd(viennaDateTime), result.dtEnd()) } @Test fun `Non-all-day event without start or end timezone`() { - val defaultTz = tzRegistry.getTimeZone(ZoneId.systemDefault().id) + val defaultTz = ZoneId.systemDefault() Assume.assumeTrue(defaultTz.id != TimeZones.UTC_ID) // would cause UTC DATE-TIME val result = VEvent() val entity = Entity(contentValuesOf( @@ -103,8 +107,11 @@ class EndTimeHandlerTest { Events.DTEND to 1592733600000L // 21/06/2020 12:00 +0200 )) handler.process(entity, entity, result) - assertEquals(1592733600000L, result.endDate?.date?.time) - assertEquals(defaultTz, (result.endDate?.date as? DateTime)?.timeZone) + assertEquals(1592733600000L, result.dtEnd()?.date?.toEpochMilli()) + assertEquals( + defaultTz.id, + result.dtEnd()?.getParameter(Parameter.TZID)?.getOrNull()?.value + ) } @Test @@ -115,8 +122,9 @@ class EndTimeHandlerTest { Events.DTSTART to 1592733600000L, // 21/06/2020 12:00 +0200; DTSTART is required for DTEND to be processed Events.EVENT_TIMEZONE to "Europe/Vienna" // will be used as end time zone )) + val viennaDateTime = ZonedDateTime.of(2020,6,21,12,0,0,0, tzVienna) handler.process(entity, entity, result) - assertEquals(DtEnd(DateTime("20200621T120000", tzVienna)), result.endDate) + assertEquals(DtEnd(viennaDateTime), result.dtEnd()) } @@ -129,7 +137,7 @@ class EndTimeHandlerTest { Events.DTEND to 1592733500000L )) handler.process(entity, entity, result) - assertNull(result.endDate) + assertNull(result.dtEnd()) } @Test @@ -140,7 +148,7 @@ class EndTimeHandlerTest { Events.DURATION to "PT1H" )) handler.process(entity, entity, result) - assertNull(result.endDate) + assertNull(result.dtEnd()) } @@ -163,6 +171,6 @@ class EndTimeHandlerTest { val start = System.currentTimeMillis() val result = handler.calculateFromDefault(start, allDay = false) assertEquals(start, result) - }*/ + } } \ No newline at end of file From 4d7460273a186fc8cc4ec32ba1205b088a9d99b4 Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 13:49:35 +0100 Subject: [PATCH 21/24] Update DurationHandler and tests --- .../mapping/calendar/AndroidEventHandler.kt | 2 +- .../calendar/handler/DurationHandler.kt | 25 ++++------- .../calendar/handler/DurationHandlerTest.kt | 45 +++++++++---------- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt index 98bbfc82..24dbc02d 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventHandler.kt @@ -66,7 +66,7 @@ class AndroidEventHandler( LocationHandler(), StartTimeHandler(), EndTimeHandler(), - DurationHandler(tzRegistry), + DurationHandler(), RecurrenceFieldsHandler(tzRegistry), DescriptionHandler(), ColorHandler(), diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt index 5c969611..3ac6a2dd 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandler.kt @@ -9,12 +9,9 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.ical4android.util.TimeApiExtensions.abs -import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate -import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime -import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime +import at.bitfire.synctools.icalendar.plusAssign +import at.bitfire.synctools.mapping.calendar.builder.AndroidTemporalMapper.toZonedDateTime import at.bitfire.synctools.util.AndroidTimeUtils -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.TimeZoneRegistry import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtEnd import java.time.Instant @@ -28,9 +25,7 @@ import java.time.ZoneOffset * - [Events.DTEND] is present / not null (because DTEND then takes precedence over DURATION), and/or * - [Events.DURATION] is null / not present. */ -class DurationHandler( - private val tzRegistry: TimeZoneRegistry -): AndroidEventFieldHandler { +class DurationHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { val values = from.entityValues @@ -52,28 +47,26 @@ class DurationHandler( val tsStart = values.getAsLong(Events.DTSTART) ?: return val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0 - TODO("ical4j 4.x") - /*if (allDay) { + if (allDay) { val startTimeUTC = Instant.ofEpochMilli(tsStart).atOffset(ZoneOffset.UTC) val endDate = (startTimeUTC + duration).toLocalDate() // DATE - to.properties += DtEnd(endDate.toIcal4jDate()) + to += DtEnd(endDate) } else { // DATE-TIME val startDateTime = AndroidTimeField( timestamp = tsStart, timeZone = values.getAsString(Events.EVENT_TIMEZONE), - allDay = false, - tzRegistry = tzRegistry - ).asIcal4jDate() as DateTime + allDay = false + ).asTemporal() val start = startDateTime.toZonedDateTime() val end = start + duration - to.properties += DtEnd(end.toIcal4jDateTime(tzRegistry)) - }*/ + to += DtEnd(end) + } } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt index 0ca6c1db..0e6511f3 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DurationHandlerTest.kt @@ -9,34 +9,29 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf +import at.bitfire.synctools.icalendar.dtEnd import junit.framework.TestCase.assertEquals -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime -import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.DtEnd import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.time.LocalDate +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.Temporal -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class DurationHandlerTest { - private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() - private val tzVienna = tzRegistry.getTimeZone("Europe/Vienna")!! + private val tzVienna = ZoneId.of("Europe/Vienna") - private val handler = DurationHandler(tzRegistry) - - init { - TODO("ical4j 4.x") - } + private val handler = DurationHandler() // Note: When the calendar provider sets a non-null DURATION, it implies that the event is recurring. - /*@Test + @Test fun `All-day event with all-day duration`() { val result = VEvent() val entity = Entity(contentValuesOf( @@ -45,7 +40,7 @@ class DurationHandlerTest { Events.DURATION to "P4D" )) handler.process(entity, entity, result) - assertEquals(DtEnd(Date("20200625")), result.endDate) + assertEquals(DtEnd(LocalDate.of(2020, 6, 25)), result.dtEnd()) assertNull(result.duration) } @@ -58,7 +53,7 @@ class DurationHandlerTest { Events.DURATION to "P-4D" )) handler.process(entity, entity, result) - assertEquals(DtEnd(Date("20200625")), result.endDate) + assertEquals(DtEnd(LocalDate.of(2020, 6, 25)), result.dtEnd()) assertNull(result.duration) } @@ -71,7 +66,7 @@ class DurationHandlerTest { Events.DURATION to "PT24H" )) handler.process(entity, entity, result) - assertEquals(DtEnd(Date("20251016")), result.endDate) + assertEquals(DtEnd(LocalDate.of(2025, 10, 16)), result.dtEnd()) assertNull(result.duration) } @@ -84,7 +79,7 @@ class DurationHandlerTest { Events.DURATION to "PT-24H" )) handler.process(entity, entity, result) - assertEquals(DtEnd(Date("20251016")), result.endDate) + assertEquals(DtEnd(LocalDate.of(2025, 10, 16)), result.dtEnd()) assertNull(result.duration) } @@ -99,7 +94,8 @@ class DurationHandlerTest { )) // DST transition at 03:00, clock is set back to 02:00 → P1D = PT25H handler.process(entity, entity, result) - assertEquals(DtEnd(DateTime("20251027T010000", tzVienna)), result.endDate) + val viennaDateTime = ZonedDateTime.of(2025, 10, 27, 1, 0, 0, 0, tzVienna) + assertEquals(DtEnd(viennaDateTime), result.dtEnd()) assertNull(result.duration) } @@ -114,7 +110,8 @@ class DurationHandlerTest { )) // DST transition at 03:00, clock is set back to 02:00 → P1D = PT25H handler.process(entity, entity, result) - assertEquals(DtEnd(DateTime("20251027T010000", tzVienna)), result.endDate) + val viennaDateTime = ZonedDateTime.of(2025, 10, 27, 1, 0, 0, 0, tzVienna) + assertEquals(DtEnd(viennaDateTime), result.dtEnd()) assertNull(result.duration) } @@ -129,7 +126,8 @@ class DurationHandlerTest { )) // DST transition at 03:00, clock is set back to 02:00 → PT24H goes one hour back handler.process(entity, entity, result) - assertEquals(DtEnd(DateTime("20251027T000000", tzVienna)), result.endDate) + val viennaDateTime = ZonedDateTime.of(2025, 10, 27, 0, 0, 0, 0, tzVienna) + assertEquals(DtEnd(viennaDateTime), result.dtEnd()) assertNull(result.duration) } @@ -144,7 +142,8 @@ class DurationHandlerTest { )) // DST transition at 03:00, clock is set back to 02:00 → PT24H goes one hour back handler.process(entity, entity, result) - assertEquals(DtEnd(DateTime("20251027T000000", tzVienna)), result.endDate) + val viennaDateTime = ZonedDateTime.of(2025, 10, 27, 0, 0, 0, 0, tzVienna) + assertEquals(DtEnd(viennaDateTime), result.dtEnd()) assertNull(result.duration) } @@ -181,8 +180,8 @@ class DurationHandlerTest { Events.EVENT_TIMEZONE to "Europe/Vienna" )) handler.process(entity, entity, result) - assertNull(result.endDate) + assertNull(result.dtEnd()) assertNull(result.duration) - }*/ + } } \ No newline at end of file From 6f1c18b9e30dadcb86048803c86418f600bd047b Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 14:18:04 +0100 Subject: [PATCH 22/24] Update DescriptionHandler and tests --- .../mapping/calendar/handler/DescriptionHandler.kt | 6 +++--- .../mapping/calendar/handler/DescriptionHandlerTest.kt | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt index 92687131..f04ff723 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandler.kt @@ -8,6 +8,7 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events +import at.bitfire.synctools.icalendar.plusAssign import at.bitfire.vcard4android.Utils.trimToNull import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Description @@ -15,10 +16,9 @@ import net.fortuna.ical4j.model.property.Description class DescriptionHandler: AndroidEventFieldHandler { override fun process(from: Entity, main: Entity, to: VEvent) { - TODO("ical4j 4.x") - /*val description = from.entityValues.getAsString(Events.DESCRIPTION).trimToNull() + val description = from.entityValues.getAsString(Events.DESCRIPTION).trimToNull() if (description != null) - to.properties += Description(description)*/ + to += Description(description) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt index c9e476c2..03f91b84 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/DescriptionHandlerTest.kt @@ -13,12 +13,10 @@ import androidx.core.content.contentValuesOf import net.fortuna.ical4j.model.component.VEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class DescriptionHandlerTest { From 3f554be3c045f350f0acd06fb00d0f482a6a037e Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 14:19:41 +0100 Subject: [PATCH 23/24] Update ColorHandler and tests --- .../mapping/calendar/handler/ColorHandler.kt | 6 +++--- .../mapping/calendar/handler/ColorHandlerTest.kt | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt index ac205816..956c798a 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandler.kt @@ -9,6 +9,7 @@ package at.bitfire.synctools.mapping.calendar.handler import android.content.Entity import android.provider.CalendarContract.Events import at.bitfire.synctools.icalendar.Css3Color +import at.bitfire.synctools.icalendar.plusAssign import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Color import java.util.logging.Logger @@ -34,9 +35,8 @@ class ColorHandler: AndroidEventFieldHandler { Css3Color.entries.firstOrNull { it.argb == color } } - TODO("ical4j 4.x") - /*if (color != null) - to.properties += Color(null, color.name)*/ + if (color != null) + to += Color(null, color.name) } } \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt index 049c0cf8..f5aacd9e 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/ColorHandlerTest.kt @@ -15,12 +15,11 @@ import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Color import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.jvm.optionals.getOrNull -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class ColorHandlerTest { @@ -31,7 +30,7 @@ class ColorHandlerTest { val result = VEvent() val entity = Entity(ContentValues()) handler.process(entity, entity, result) - assertNull(result.getProperty(Color.PROPERTY_NAME)) + assertNull(result.getProperty(Color.PROPERTY_NAME).getOrNull()) } @Test @@ -41,8 +40,7 @@ class ColorHandlerTest { Events.EVENT_COLOR_KEY to Css3Color.silver.name )) handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals("silver", result.getProperty(Color.PROPERTY_NAME).value) + assertEquals("silver", result.getProperty(Color.PROPERTY_NAME)?.getOrNull()?.value) } @Test @@ -52,8 +50,7 @@ class ColorHandlerTest { Events.EVENT_COLOR to Css3Color.silver.argb )) handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals("silver", result.getProperty(Color.PROPERTY_NAME).value) + assertEquals("silver", result.getProperty(Color.PROPERTY_NAME)?.getOrNull()?.value) } } \ No newline at end of file From 3a954441c0ecfdb52a3c50db9b0a6300f772b96a Mon Sep 17 00:00:00 2001 From: Sunik Kupfer Date: Tue, 17 Mar 2026 14:21:31 +0100 Subject: [PATCH 24/24] Update AccessLevelHandler and tests --- .../calendar/handler/AccessLevelHandler.kt | 15 ++++++++------- .../calendar/handler/AccessLevelHandlerTest.kt | 12 ++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt index 89d0ad1a..ccd26219 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandler.kt @@ -10,8 +10,10 @@ import android.content.Entity import android.provider.CalendarContract.Events import android.provider.CalendarContract.ExtendedProperties import at.bitfire.ical4android.UnknownProperty +import at.bitfire.synctools.icalendar.plusAssign import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.immutable.ImmutableClazz import org.json.JSONException class AccessLevelHandler: AndroidEventFieldHandler { @@ -20,22 +22,21 @@ class AccessLevelHandler: AndroidEventFieldHandler { val values = from.entityValues // take classification from main row - TODO("ical4j 4.x") - /*val classification = when (values.getAsInteger(Events.ACCESS_LEVEL)) { + val classification = when (values.getAsInteger(Events.ACCESS_LEVEL)) { Events.ACCESS_PUBLIC -> - Clazz.PUBLIC + ImmutableClazz.PUBLIC Events.ACCESS_PRIVATE -> - Clazz.PRIVATE + ImmutableClazz.PRIVATE Events.ACCESS_CONFIDENTIAL -> - Clazz.CONFIDENTIAL + ImmutableClazz.CONFIDENTIAL - else *//* Events.ACCESS_DEFAULT *//* -> + else /* Events.ACCESS_DEFAULT */ -> retainedClassification(from) } if (classification != null) - to.properties += classification*/ + to += classification } private fun retainedClassification(from: Entity): Clazz? { diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt index 77ae8b3f..93d418bc 100644 --- a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/handler/AccessLevelHandlerTest.kt @@ -14,14 +14,13 @@ import androidx.core.content.contentValuesOf import at.bitfire.ical4android.UnknownProperty import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Clazz +import net.fortuna.ical4j.model.property.immutable.ImmutableClazz import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@Ignore("ical4j 4.x") @RunWith(RobolectricTestRunner::class) class AccessLevelHandlerTest { @@ -78,8 +77,7 @@ class AccessLevelHandlerTest { Events.ACCESS_LEVEL to Events.ACCESS_PUBLIC )) handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals(Clazz.PUBLIC, result.classification) + assertEquals(ImmutableClazz.PUBLIC, result.classification) } @Test @@ -89,8 +87,7 @@ class AccessLevelHandlerTest { Events.ACCESS_LEVEL to Events.ACCESS_PRIVATE )) handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals(Clazz.PRIVATE, result.classification) + assertEquals(ImmutableClazz.PRIVATE, result.classification) } @Test @@ -100,8 +97,7 @@ class AccessLevelHandlerTest { Events.ACCESS_LEVEL to Events.ACCESS_CONFIDENTIAL )) handler.process(entity, entity, result) - TODO("ical4j 4.x") - //assertEquals(Clazz.CONFIDENTIAL, result.classification) + assertEquals(ImmutableClazz.CONFIDENTIAL, result.classification) } } \ No newline at end of file