Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion lib/src/main/kotlin/at/bitfire/ical4android/Event.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import java.util.LinkedList
* - as it is extracted from an iCalendar or
* - as it should be generated into an iCalendar.
*/
@Deprecated("Use net.fortuna.ical4j.model.Calendar instead")
@Deprecated("AssociatedEvents instead", replaceWith = ReplaceWith("AssociatedEvents"))
data class Event(
override var uid: String? = null,
override var sequence: Int? = null,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ import net.fortuna.ical4j.model.component.VEvent
* @param exceptions exceptions (each without RECURRENCE-ID); UID must be
* 1. the same as the UID of [main],
* 2. the same for all exceptions.
* @param prodId optional `PRODID` related to the components
*
* If no [main] is present, [exceptions] must not be empty.
*
* @throws IllegalArgumentException when the constraints above are violated
*/
data class AssociatedComponents<T: CalendarComponent>(
val main: T?,
val exceptions: List<T>
val exceptions: List<T>,
val prodId: String? = null
) {

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package at.bitfire.synctools.mapping.calendar

import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import at.bitfire.synctools.icalendar.AssociatedEvents
import at.bitfire.synctools.mapping.calendar.processor.AccessLevelProcessor
import at.bitfire.synctools.mapping.calendar.processor.AndroidEventFieldProcessor
import at.bitfire.synctools.mapping.calendar.processor.AttendeesProcessor
Expand All @@ -19,9 +19,9 @@ import at.bitfire.synctools.mapping.calendar.processor.DescriptionProcessor
import at.bitfire.synctools.mapping.calendar.processor.DurationProcessor
import at.bitfire.synctools.mapping.calendar.processor.EndTimeProcessor
import at.bitfire.synctools.mapping.calendar.processor.LocationProcessor
import at.bitfire.synctools.mapping.calendar.processor.MutatorsProcessor
import at.bitfire.synctools.mapping.calendar.processor.OrganizerProcessor
import at.bitfire.synctools.mapping.calendar.processor.OriginalInstanceTimeProcessor
import at.bitfire.synctools.mapping.calendar.processor.ProdIdGenerator
import at.bitfire.synctools.mapping.calendar.processor.RecurrenceFieldsProcessor
import at.bitfire.synctools.mapping.calendar.processor.RemindersProcessor
import at.bitfire.synctools.mapping.calendar.processor.SequenceProcessor
Expand All @@ -33,29 +33,31 @@ import at.bitfire.synctools.mapping.calendar.processor.UnknownPropertiesProcesso
import at.bitfire.synctools.mapping.calendar.processor.UrlProcessor
import at.bitfire.synctools.storage.calendar.EventAndExceptions
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.RDate
import net.fortuna.ical4j.model.property.RRule
import net.fortuna.ical4j.model.property.RecurrenceId
import java.util.LinkedList

/**
* Legacy mapper from Android event main + data rows to an [Event]
* (former "populate..." methods).
* Mapper from Android event main + data rows to [VEvent].
*
* Important: To use recurrence exceptions, you MUST set _SYNC_ID and ORIGINAL_SYNC_ID
* in populateEvent() / buildEvent. Setting _ID and ORIGINAL_ID is not sufficient.
*
* @param accountName account name (used to generate self-attendee)
* @param accountName account name (used to generate self-attendee)
* @param prodIdGenerator generator for `PRODID`
*/
class LegacyAndroidEventProcessor(
private val accountName: String
class AndroidEventProcessor(
accountName: String,
private val prodIdGenerator: ProdIdGenerator
) {

private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()

private val fieldProcessors: Array<AndroidEventFieldProcessor> = arrayOf(
// event row fields
MutatorsProcessor(), // for PRODID
UidProcessor(),
OriginalInstanceTimeProcessor(tzRegistry),
TitleProcessor(),
Expand All @@ -82,46 +84,51 @@ class LegacyAndroidEventProcessor(
)


fun populate(eventAndExceptions: EventAndExceptions, to: Event) {
fun populate(eventAndExceptions: EventAndExceptions): AssociatedEvents {
// main event
populateEvent(
val main = populateEvent(
entity = eventAndExceptions.main,
main = eventAndExceptions.main,
to = to
main = eventAndExceptions.main
)

// Add exceptions of recurring main event
if (to.rRules.isNotEmpty() || to.rDates.isNotEmpty()) {
val rRules = main.getProperties<RRule>(Property.RRULE)
val rDates = main.getProperties<RDate>(Property.RDATE)
val exceptions = LinkedList<VEvent>()
if (rRules.isNotEmpty() || rDates.isNotEmpty()) {
for (exception in eventAndExceptions.exceptions) {
val exceptionEvent = Event()

// convert exception to Event
populateEvent(
val exceptionEvent = populateEvent(
entity = exception,
main = eventAndExceptions.main,
to = exceptionEvent
main = eventAndExceptions.main
)

// make sure that exception has a RECURRENCE-ID
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)
addAsExDate(exception, recurrenceId, to = to)
main.properties += asExDate(exception, recurrenceId)
else
to.exceptions += exceptionEvent
exceptions += exceptionEvent
}
}

return AssociatedEvents(
main = main,
exceptions = exceptions,
prodId = generateProdId(eventAndExceptions.main)
)
}

private fun addAsExDate(entity: Entity, recurrenceId: RecurrenceId, to: Event) {
private fun asExDate(entity: Entity, recurrenceId: RecurrenceId): ExDate {
val originalAllDay = (entity.entityValues.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0
val list = DateList(
if (originalAllDay) Value.DATE else Value.DATE_TIME,
recurrenceId.timeZone
)
list.add(recurrenceId.date)
to.exDates += ExDate(list).apply {
return ExDate(list).apply {
// also set TZ properties of ExDate (not only the list)
if (!originalAllDay) {
if (recurrenceId.isUtc)
Expand All @@ -132,18 +139,35 @@ class LegacyAndroidEventProcessor(
}
}

private fun generateProdId(main: Entity): String {
val mutators: String? = main.entityValues.getAsString(Events.MUTATORS)
val packages: List<String> = mutators?.split(MUTATORS_SEPARATOR)?.toList() ?: emptyList()
return prodIdGenerator.generateProdId(packages)
}

/**
* Reads data of an event from the calendar provider, i.e. converts the [entity] values into
* an [Event] data object.
* Reads data of an event from the calendar provider, i.e. converts the [entity] values into a [VEvent].
*
* @param entity event row as returned by the calendar provider
* @param main main event row as returned by the calendar provider
* @param to destination data object
*
* @return generated data object
*/
private fun populateEvent(entity: Entity, main: Entity, to: Event) {
// new processors
private fun populateEvent(entity: Entity, main: Entity): VEvent {
val vEvent = VEvent()
for (processor in fieldProcessors)
processor.process(from = entity, main = main, to = to)
processor.process(from = entity, main = main, to = vEvent)
return vEvent
}


companion object {

/**
* The [Events.MUTATORS] field contains a list of unique package names that have modified the event,
* separated by this separator.
*/
const val MUTATORS_SEPARATOR = ','

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import at.bitfire.synctools.storage.calendar.AndroidEvent2
class SequenceBuilder: AndroidEntityBuilder {

override fun build(from: Event, main: Event, to: Entity) {
to.entityValues.put(AndroidEvent2.COLUMN_SEQUENCE, from.sequence)
/* When we build the SEQUENCE column from a real event, we set the sequence to 0 (not null), so that we
can distinguish it from events which have been created locally and have never been uploaded yet. */
to.entityValues.put(AndroidEvent2.COLUMN_SEQUENCE, from.sequence ?: 0)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ package at.bitfire.synctools.mapping.calendar.processor
import android.content.Entity
import android.provider.CalendarContract.Events
import android.provider.CalendarContract.ExtendedProperties
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.UnknownProperty
import net.fortuna.ical4j.model.component.VEvent
import net.fortuna.ical4j.model.property.Clazz
import org.json.JSONException

class AccessLevelProcessor: AndroidEventFieldProcessor {

override fun process(from: Entity, main: Entity, to: Event) {
override fun process(from: Entity, main: Entity, to: VEvent) {
val values = from.entityValues

// take classification from main row
to.classification = when (values.getAsInteger(Events.ACCESS_LEVEL)) {
val classification = when (values.getAsInteger(Events.ACCESS_LEVEL)) {
Events.ACCESS_PUBLIC ->
Clazz.PUBLIC

Expand All @@ -33,6 +33,8 @@ class AccessLevelProcessor: AndroidEventFieldProcessor {
else /* Events.ACCESS_DEFAULT */ ->
retainedClassification(from)
}
if (classification != null)
to.properties += classification
}

private fun retainedClassification(from: Entity): Clazz? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package at.bitfire.synctools.mapping.calendar.processor
import android.content.Entity
import at.bitfire.ical4android.Event
import at.bitfire.synctools.exception.InvalidLocalResourceException
import net.fortuna.ical4j.model.component.VEvent

interface AndroidEventFieldProcessor {

Expand All @@ -35,6 +36,6 @@ interface AndroidEventFieldProcessor {
*
* @throws InvalidLocalResourceException on missing or invalid required fields (like [android.provider.CalendarContract.Events.DTSTART])
*/
fun process(from: Entity, main: Entity, to: Event)
fun process(from: Entity, main: Entity, to: VEvent)

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ package at.bitfire.synctools.mapping.calendar.processor
import android.content.ContentValues
import android.content.Entity
import android.provider.CalendarContract.Attendees
import at.bitfire.ical4android.Event
import at.bitfire.synctools.mapping.calendar.AttendeeMappings
import net.fortuna.ical4j.model.component.VEvent
import net.fortuna.ical4j.model.parameter.Cn
import net.fortuna.ical4j.model.parameter.Email
import net.fortuna.ical4j.model.parameter.PartStat
Expand All @@ -26,12 +26,12 @@ class AttendeesProcessor: AndroidEventFieldProcessor {
private val logger
get() = Logger.getLogger(javaClass.name)

override fun process(from: Entity, main: Entity, to: Event) {
override fun process(from: Entity, main: Entity, to: VEvent) {
for (row in from.subValues.filter { it.uri == Attendees.CONTENT_URI })
populateAttendee(row.values, to)
}

private fun populateAttendee(row: ContentValues, to: Event) {
private fun populateAttendee(row: ContentValues, to: VEvent) {
logger.log(Level.FINE, "Read event attendee from calendar provider", row)

try {
Expand Down Expand Up @@ -66,7 +66,7 @@ class AttendeesProcessor: AndroidEventFieldProcessor {
Attendees.ATTENDEE_STATUS_NONE -> { /* no information, don't add PARTSTAT */ }
}

to.attendees.add(attendee)
to.properties += attendee
} catch (e: URISyntaxException) {
logger.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,25 @@ package at.bitfire.synctools.mapping.calendar.processor

import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import net.fortuna.ical4j.model.component.VEvent
import net.fortuna.ical4j.model.property.Transp

class AvailabilityProcessor: AndroidEventFieldProcessor {

override fun process(from: Entity, main: Entity, to: Event) {
to.opaque = from.entityValues.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE
override fun process(from: Entity, main: Entity, to: VEvent) {
val transp = when (from.entityValues.getAsInteger(Events.AVAILABILITY)) {
Events.AVAILABILITY_BUSY,
Events.AVAILABILITY_TENTATIVE ->
Transp.OPAQUE

Events.AVAILABILITY_FREE ->
Transp.TRANSPARENT

else ->
null // defaults to OPAQUE in iCalendar
}
if (transp != null)
to.properties += transp
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ package at.bitfire.synctools.mapping.calendar.processor

import android.content.Entity
import android.provider.CalendarContract.ExtendedProperties
import at.bitfire.ical4android.Event
import at.bitfire.synctools.storage.calendar.AndroidEvent2
import net.fortuna.ical4j.model.TextList
import net.fortuna.ical4j.model.component.VEvent
import net.fortuna.ical4j.model.property.Categories

class CategoriesProcessor: AndroidEventFieldProcessor {

override fun process(from: Entity, main: Entity, to: Event) {
override fun process(from: Entity, main: Entity, to: VEvent) {
val extended = from.subValues.filter { it.uri == ExtendedProperties.CONTENT_URI }.map { it.values }
val categories = extended.firstOrNull { it.getAsString(ExtendedProperties.NAME) == AndroidEvent2.EXTNAME_CATEGORIES }
val listValue = categories?.getAsString(ExtendedProperties.VALUE)
if (listValue != null)
to.categories += listValue.split(AndroidEvent2.CATEGORIES_SEPARATOR)
if (listValue != null) {
to.properties += Categories(TextList(
listValue.split(AndroidEvent2.CATEGORIES_SEPARATOR).toTypedArray()
))
}
}

}
Loading