Skip to content
Merged
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
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("Use AssociatedEvents instead", replaceWith = ReplaceWith("AssociatedEvents", "at.bitfire.synctools.icalendar"))
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