Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
)
}

}
Original file line number Diff line number Diff line change
@@ -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
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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())
}
26 changes: 26 additions & 0 deletions lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<VEvent>
) {

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")
}

}
Original file line number Diff line number Diff line change
@@ -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<Uid?, AssociatedVEvents> {
val vEvents = calendar.getComponents<VEvent>(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<Uid?, List<VEvent>> = vEvents.groupBy { it.uid }

// TODO reduce to highest SEQUENCE

val result = mutableMapOf<Uid?, AssociatedVEvents>()
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
}

}
Original file line number Diff line number Diff line change
@@ -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<Property>().apply {
properties.forEach { add(it) }
}

fun Date.isAllDay(): Boolean =
this !is DateTime
Loading