diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalInstanceTimeBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalInstanceTimeBuilderTest.kt new file mode 100644 index 00000000..6badad1a --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalInstanceTimeBuilderTest.kt @@ -0,0 +1,61 @@ +/* + * 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.mapper.calendar.builder + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.synctools.mapper.calendar.propertyListOf +import at.bitfire.synctools.test.assertEntityEquals +import io.mockk.mockk +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.RecurrenceId +import org.junit.Test + +class OriginalInstanceTimeBuilderTest { + + @Test + fun testRecurId_AllDay() { + val entity = Entity(ContentValues()) + val vEvent = VEvent(propertyListOf( + RecurrenceId(Date("20250628")) + )) + OriginalInstanceTimeBuilder.intoEntity(mockk(), vEvent, vEvent, entity) + assertEntityEquals( + Entity(contentValuesOf(Events.ORIGINAL_INSTANCE_TIME to 1751068800000)), + entity + ) + } + + @Test + fun testRecurId_TimeUTC() { + val entity = Entity(ContentValues()) + val vEvent = VEvent(propertyListOf( + RecurrenceId(DateTime("20250628T010203Z")) + )) + OriginalInstanceTimeBuilder.intoEntity(mockk(), vEvent, vEvent, entity) + assertEntityEquals( + Entity(contentValuesOf(Events.ORIGINAL_INSTANCE_TIME to 1751072523000)), + entity + ) + } + + @Test + fun testRecurId_Null() { + val entity = Entity(ContentValues()) + val vEvent = VEvent() + OriginalInstanceTimeBuilder.intoEntity(mockk(), vEvent, vEvent, entity) + assertEntityEquals( + Entity(contentValuesOf()), + entity + ) + } + +} \ No newline at end of file diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapper/calendar/builder/UidBuilderTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapper/calendar/builder/UidBuilderTest.kt new file mode 100644 index 00000000..ab66ff10 --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapper/calendar/builder/UidBuilderTest.kt @@ -0,0 +1,45 @@ +/* + * 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.mapper.calendar.builder + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.synctools.test.assertEntityEquals +import io.mockk.mockk +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.property.Uid +import org.junit.Test + +class UidBuilderTest { + + @Test + fun testUid_Null() { + val entity = Entity(ContentValues()) + val vEvent = VEvent() + UidBuilder.intoEntity(mockk(), vEvent, vEvent, entity) + assertEntityEquals( + Entity(contentValuesOf(Events.UID_2445 to null)), + entity + ) + } + + @Test + fun testUid_Value() { + val entity = Entity(ContentValues()) + val vEvent = VEvent().apply { + properties.add(Uid("test@12345")) + } + UidBuilder.intoEntity(mockk(), vEvent, vEvent, entity) + assertEntityEquals( + Entity(contentValuesOf(Events.UID_2445 to "test@12345")), + entity + ) + } + +} \ No newline at end of file diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/CalendarBatchOperationTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/CalendarBatchOperationTest.kt index a19c97c3..a896ab35 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/CalendarBatchOperationTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/storage/CalendarBatchOperationTest.kt @@ -15,6 +15,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.closeCompat +import at.bitfire.synctools.storage.calendar.CalendarBatchOperation import org.junit.After import org.junit.Before import org.junit.Rule diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/test/EntityHelpers.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/test/EntityHelpers.kt new file mode 100644 index 00000000..09273b1b --- /dev/null +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/test/EntityHelpers.kt @@ -0,0 +1,18 @@ +/* + * 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.test + +import android.content.Entity +import org.junit.ComparisonFailure + +fun assertEntityEquals(expected: Entity, actual: Entity, message: String? = null) { + val entityValuesEqual = actual.entityValues == expected.entityValues + val subValuesEqual = expected.subValues.toSet() == actual.subValues.toSet() + + if (!entityValuesEqual || !subValuesEqual) + throw ComparisonFailure(message, expected.toString(), actual.toString()) +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt index c421637f..4fad82dc 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt @@ -21,6 +21,9 @@ import androidx.core.content.contentValuesOf import at.bitfire.ical4android.AndroidCalendar.Companion.find import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.toValues +import at.bitfire.synctools.storage.BatchOperation +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.bitfire.synctools.storage.calendar.CalendarBatchOperation import java.io.FileNotFoundException import java.util.LinkedList import java.util.logging.Level @@ -128,6 +131,29 @@ class AndroidCalendar( } + // AndroidEvent2 CRUD + + fun add(event: AndroidEvent2) { + val batch = CalendarBatchOperation(provider) + + /* main event */ + val mainEvent = event.mainEvent + + // event row + batch += BatchOperation.CpoBuilder + .newInsert(Events.CONTENT_URI.asSyncAdapter(account)) + .withValues(mainEvent.entityValues) + + // other rows: reminders etc. + + /* exceptions */ + + batch.commit() + } + + + // helpers + fun calendarSyncURI() = ContentUris.withAppendedId(Calendars.CONTENT_URI, id).asSyncAdapter(account) diff --git a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt index 00608a25..a7179ea7 100644 --- a/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt +++ b/lib/src/main/kotlin/at/bitfire/ical4android/AndroidEvent.kt @@ -39,8 +39,8 @@ import at.bitfire.ical4android.util.TimeApiExtensions.toLocalTime import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime import at.bitfire.synctools.storage.BatchOperation.CpoBuilder -import at.bitfire.synctools.storage.CalendarBatchOperation import at.bitfire.synctools.storage.LocalStorageException +import at.bitfire.synctools.storage.calendar.CalendarBatchOperation import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/AssociatedVEvents.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/AssociatedVEvents.kt new file mode 100644 index 00000000..5eeeffc1 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/AssociatedVEvents.kt @@ -0,0 +1,64 @@ +/* + * 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.mapper.calendar + +import net.fortuna.ical4j.model.component.VEvent + +/** + * @param mainVEvent main VEVENT (with UID, without RECURRENCE-ID), may be `null` if only exceptions are present + * @param exceptions exceptions (each with UID and RECURRENCE-ID); UID must be + * 1. the same as the UID of [mainVEvent] (if present), + * 2. the same for all exceptions. + * + * If no [mainVEvent] is present, [exceptions] must not be empty. + * + * @throws IllegalArgumentException when the constraints above are violated + */ +data class AssociatedVEvents( + val mainVEvent: VEvent?, + val exceptions: List +) { + + init { + validate() + } + + /** + * Validates the requirements of [mainVEvent] and [exceptions] UIDs. + * + * @throws IllegalArgumentException if [mainVEvent] and/or [exceptions] UIDs don't match + */ + private fun validate() { + val mainUid = + if (mainVEvent != null) { + if (mainVEvent.uid == null) + throw IllegalArgumentException("Main event must have an UID") + if (mainVEvent.recurrenceId != null) + throw IllegalArgumentException("Main event must not have a RECURRENCE-ID") + + mainVEvent.uid + } + else + null + + val exceptionsUid = + if (exceptions.isNotEmpty()) { + if (exceptions.any { it.recurrenceId == null } ) + throw IllegalArgumentException("Exceptions must have RECURRENCE-ID") + + val firstExceptionUid = exceptions.first().uid + if (exceptions.any { it.uid != firstExceptionUid }) + throw IllegalArgumentException("Exceptions must not have different UIDs") + firstExceptionUid + } else + null + + if (mainUid != null && exceptionsUid != null && exceptionsUid != mainUid) + throw IllegalArgumentException("Exceptions must have the same UID as the main event") + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/CalendarUidSplitter.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/CalendarUidSplitter.kt new file mode 100644 index 00000000..deec5699 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/CalendarUidSplitter.kt @@ -0,0 +1,40 @@ +/* + * 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.mapper.calendar + +import net.fortuna.ical4j.model.Calendar +import net.fortuna.ical4j.model.Component +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.property.Uid + +/** + * Splits ICalendar components by UID. + */ +class CalendarUidSplitter( + private val calendar: Calendar +) { + + fun associateEvents(): Map { + val vEvents = calendar.getComponents(Component.VEVENT).toMutableList() + + // Note: 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> = vEvents.groupBy { it.uid } + + // TODO reduce to highest SEQUENCE + + val result = mutableMapOf() + for ((uid, vEventsWithUid) in byUid) { + val mainVEvent = vEventsWithUid.last { it.recurrenceId == null } + val exceptions = vEventsWithUid.filter { it.recurrenceId != null } + result[uid] = AssociatedVEvents(mainVEvent, exceptions) + } + + return result + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/Ical4jHelpers.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/Ical4jHelpers.kt new file mode 100644 index 00000000..59c84d13 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/Ical4jHelpers.kt @@ -0,0 +1,25 @@ +/* + * 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.mapper.calendar + +import net.fortuna.ical4j.model.Date +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 + +fun parameterListOf(vararg parameters: Parameter) = ParameterList().apply { + parameters.forEach { add(it) } +} + +fun propertyListOf(vararg properties: Property) = PropertyList().apply { + properties.forEach { add(it) } +} + +fun Date.isAllDay(): Boolean = + this !is DateTime \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/AndroidEvent2Builder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/AndroidEvent2Builder.kt new file mode 100644 index 00000000..aa72775e --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/AndroidEvent2Builder.kt @@ -0,0 +1,110 @@ +/* + * 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.mapper.calendar.builder + +import android.content.ContentValues +import android.content.Entity +import at.bitfire.synctools.mapper.calendar.AssociatedVEvents +import at.bitfire.synctools.mapper.calendar.propertyListOf +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.model.property.Summary + +/** + * Converts VEVENTs from an calendar object resource (RFC 4791 Section 4.1) to an + * Android calendar provider representation ("Android event"). + * + * @param syncProperties sync properties that are not contained in the iCalendar but shall be present in the Android event. + + */ +class AndroidEvent2Builder( + private val syncProperties: SyncProperties, + private val vEvents: AssociatedVEvents +) { + + /* TBD Features: + + - uid, recurrence-id, sequence + - start time generator + - end time (/ duration) generator + - summary, location, URL, description + - color, image, conference + - categories + - recurrence rules (RRULE, RDATE, EXRULE, EXDATE) + - organizer, attendees + - status, transparency, classification + - alarms + - attachments + - unknown properties + */ + + /** + * Builds the Android event from the [syncProperties], the [mainVEvent] and the [exceptions]. + * + * @throws IllegalArgumentException if [mainVEvent] and/or [exceptions] UIDs don't match + */ + fun build(): AndroidEvent2 { + val mainVEvent = vEvents.mainVEvent ?: fakeMainEvent() + + return AndroidEvent2( + mainEvent = buildEvent(mainVEvent, mainVEvent).also { entity -> + // apply sync properties like file name, flags, dirty/deleted + SyncPropertiesBuilder.intoEntity(syncProperties, entity) + }, + exceptions = vEvents.exceptions.map { exception -> + buildEvent(exception, mainVEvent) + } + ) + } + + /** + * Generate fake main event from the list of exceptions. + */ + private fun fakeMainEvent(): VEvent { + return VEvent(propertyListOf( + vEvents.exceptions.first().uid, + Summary("(unknown recurring event)") + // DTSTART = DTSTART of first exception + // RDATE = DTSTARTs of all exceptions + // etc. + )) + } + + private fun buildEvent(vEvent: VEvent, mainVEvent: VEvent): Entity { + val entity = Entity(ContentValues()) + for (feature in features) + feature.intoEntity(syncProperties, vEvent, mainVEvent, entity) + return entity + } + + + data class SyncProperties( + val calendarId: Long, + val fileName: String, + val dirty: Boolean = false, + val deleted: Boolean = false, + val flags: Int = 0 + ) + + + companion object { + + val features = arrayOf( + DescriptionBuilder, + DtEndBuilder, + DtStartBuilder, + EventTimeZoneBuilder, + OriginalInstanceTimeBuilder, + OriginalSyncIdBuilder, + SummaryBuilder, + // special case: SyncPropertiesBuilder is explicitly called by build() + UidBuilder + ) + + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DescriptionBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DescriptionBuilder.kt new file mode 100644 index 00000000..caa80725 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DescriptionBuilder.kt @@ -0,0 +1,19 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.component.VEvent + +object DescriptionBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + entity.entityValues.put(Events.DESCRIPTION, "Synced by new algorithm") + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DtEndBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DtEndBuilder.kt new file mode 100644 index 00000000..0ac28770 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DtEndBuilder.kt @@ -0,0 +1,23 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.component.VEvent + +object DtEndBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + val endDate = vEvent.endDate + if (endDate == null) + return + + entity.entityValues.put(Events.DTEND, endDate.date.time) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DtStartBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DtStartBuilder.kt new file mode 100644 index 00000000..ed101927 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/DtStartBuilder.kt @@ -0,0 +1,23 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.component.VEvent + +object DtStartBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + val startDate = vEvent.startDate + if (startDate == null) + return + + entity.entityValues.put(Events.DTSTART, startDate.date.time) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/EventTimeZoneBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/EventTimeZoneBuilder.kt new file mode 100644 index 00000000..47eb6938 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/EventTimeZoneBuilder.kt @@ -0,0 +1,28 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.component.VEvent +import net.fortuna.ical4j.util.TimeZones + +object EventTimeZoneBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + val startDate = vEvent.startDate?.date + + val timeZone = if (startDate != null && startDate is DateTime) + startDate.timeZone.id // TBD only use Android time zones + else + TimeZones.UTC_ID + + entity.entityValues.put(Events.EVENT_TIMEZONE, timeZone) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/FeatureBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/FeatureBuilder.kt new file mode 100644 index 00000000..862139df --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/FeatureBuilder.kt @@ -0,0 +1,16 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import net.fortuna.ical4j.model.component.VEvent + +interface FeatureBuilder { + + fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalInstanceTimeBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalInstanceTimeBuilder.kt new file mode 100644 index 00000000..70324dea --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalInstanceTimeBuilder.kt @@ -0,0 +1,23 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.component.VEvent + +object OriginalInstanceTimeBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + val recurrenceId = vEvent.recurrenceId + if (recurrenceId == null) + return + + entity.entityValues.put(Events.ORIGINAL_INSTANCE_TIME, recurrenceId.date.time) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalSyncIdBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalSyncIdBuilder.kt new file mode 100644 index 00000000..f438e55c --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/OriginalSyncIdBuilder.kt @@ -0,0 +1,21 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.component.VEvent + +object OriginalSyncIdBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + val isException = vEvent.recurrenceId != null + if (isException) + entity.entityValues.put(Events.ORIGINAL_SYNC_ID, syncProperties.fileName) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/SummaryBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/SummaryBuilder.kt new file mode 100644 index 00000000..63e63635 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/SummaryBuilder.kt @@ -0,0 +1,19 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.component.VEvent + +object SummaryBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + entity.entityValues.put(Events.TITLE, vEvent.summary?.value) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/SyncPropertiesBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/SyncPropertiesBuilder.kt new file mode 100644 index 00000000..79182c2e --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/SyncPropertiesBuilder.kt @@ -0,0 +1,23 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.AndroidEvent + +object SyncPropertiesBuilder { + + fun intoEntity(properties: AndroidEvent2Builder.SyncProperties, entity: Entity) { + entity.entityValues.put(Events.CALENDAR_ID, properties.calendarId) + entity.entityValues.put(Events._SYNC_ID, properties.fileName) + entity.entityValues.put(Events.DIRTY, if (properties.dirty) 1 else 0) + entity.entityValues.put(Events.DELETED, if (properties.deleted) 1 else 0) + entity.entityValues.put(AndroidEvent.COLUMN_FLAGS, properties.flags) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/UidBuilder.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/UidBuilder.kt new file mode 100644 index 00000000..014f4a84 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/builder/UidBuilder.kt @@ -0,0 +1,19 @@ +/* + * 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.mapper.calendar.builder + +import android.content.Entity +import android.provider.CalendarContract.Events +import net.fortuna.ical4j.model.component.VEvent + +object UidBuilder: FeatureBuilder { + + override fun intoEntity(syncProperties: AndroidEvent2Builder.SyncProperties, vEvent: VEvent, mainVEvent: VEvent, entity: Entity) { + entity.entityValues.put(Events.UID_2445, mainVEvent.uid?.value) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/processor/AndroidEventProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/processor/AndroidEventProcessor.kt new file mode 100644 index 00000000..92d8a363 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapper/calendar/processor/AndroidEventProcessor.kt @@ -0,0 +1,20 @@ +/* + * 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.mapper.calendar.processor + +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import net.fortuna.ical4j.model.Calendar + +class AndroidEventProcessor( + val androidEvent: AndroidEvent2 +) { + + fun toVEvents(): Calendar { + TODO() + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/BatchOperation.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/BatchOperation.kt index f90e23c9..e38205bd 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/BatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/BatchOperation.kt @@ -10,6 +10,7 @@ import android.content.ContentProviderClient import android.content.ContentProviderOperation import android.content.ContentProviderResult import android.content.ContentUris +import android.content.ContentValues import android.content.OperationApplicationException import android.net.Uri import android.os.RemoteException @@ -264,6 +265,13 @@ open class BatchOperation internal constructor( return this } + fun withValues(values: ContentValues): CpoBuilder { + for ((key, value) in values.valueSet()) + if (key != null) + _values[key] = value + return this + } + fun withYieldAllowed() { yieldAllowed = true } diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/AndroidEvent2.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/AndroidEvent2.kt new file mode 100644 index 00000000..52fd1a6c --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/AndroidEvent2.kt @@ -0,0 +1,49 @@ +/* + * 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.storage.calendar + +import android.content.Entity +import android.provider.CalendarContract.Events + +/** + * Represents an event in the Android calendar provider. + */ +data class AndroidEvent2( + val mainEvent: Entity, + val exceptions: List +) { + + val id: Long? + get() = mainEvent.entityValues?.getAsLong(Events._ID) + + val syncId: String? + get() = mainEvent.entityValues.getAsString(Events._SYNC_ID) + + val eTag: String? + get() = mainEvent.entityValues.getAsString(COLUMN_ETAG) + + val flags: Int + get() = mainEvent.entityValues.getAsInteger(COLUMN_FLAGS) ?: 0 + + + companion object { + + /** + * Custom sync column to store the last known ETag of an event. + */ + const val COLUMN_ETAG = Events.SYNC_DATA1 + + /** + * Custom sync column to store sync flags of an event. + */ + const val COLUMN_FLAGS = Events.SYNC_DATA2 + + const val FLAGS_REMOTELY_PRESENT = 1 + + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/storage/CalendarBatchOperation.kt b/lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/CalendarBatchOperation.kt similarity index 83% rename from lib/src/main/kotlin/at/bitfire/synctools/storage/CalendarBatchOperation.kt rename to lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/CalendarBatchOperation.kt index dae280f8..c2d7d0f3 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/storage/CalendarBatchOperation.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/CalendarBatchOperation.kt @@ -4,9 +4,10 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -package at.bitfire.synctools.storage +package at.bitfire.synctools.storage.calendar import android.content.ContentProviderClient +import at.bitfire.synctools.storage.BatchOperation /** * [BatchOperation] for the Android calendar provider diff --git a/lib/src/main/resources/ical4j.properties b/lib/src/main/resources/ical4j.properties index edc3d429..31fb64ae 100644 --- a/lib/src/main/resources/ical4j.properties +++ b/lib/src/main/resources/ical4j.properties @@ -1,8 +1,9 @@ net.fortuna.ical4j.timezone.cache.impl=net.fortuna.ical4j.util.MapTimeZoneCache +net.fortuna.ical4j.timezone.date.floating=false 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 ical4j.validation.relaxed=true -ical4j.compatibility.outlook=true +ical4j.compatibility.outlook=true \ No newline at end of file