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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import android.content.Entity
import android.provider.CalendarContract.Attendees
import android.provider.CalendarContract.Events
import android.provider.CalendarContract.ExtendedProperties
import android.provider.CalendarContract.Reminders
import android.util.Patterns
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.UnknownProperty
import at.bitfire.ical4android.util.AndroidTimeUtils
Expand All @@ -21,22 +19,18 @@ import at.bitfire.ical4android.util.TimeApiExtensions
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
import at.bitfire.synctools.exception.InvalidLocalResourceException
import at.bitfire.synctools.icalendar.Css3Color
import at.bitfire.synctools.mapping.calendar.processor.AndroidEventFieldProcessor
import at.bitfire.synctools.mapping.calendar.processor.AttendeesProcessor
import at.bitfire.synctools.mapping.calendar.processor.RemindersProcessor
import at.bitfire.synctools.mapping.calendar.processor.UidProcessor
import at.bitfire.synctools.storage.calendar.AndroidEvent2
import at.bitfire.synctools.storage.calendar.EventAndExceptions
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.component.VAlarm
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.parameter.Rsvp
import net.fortuna.ical4j.model.parameter.Value
import net.fortuna.ical4j.model.property.Action
import net.fortuna.ical4j.model.property.Attendee
import net.fortuna.ical4j.model.property.Clazz
import net.fortuna.ical4j.model.property.Description
import net.fortuna.ical4j.model.property.DtEnd
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.ExDate
Expand All @@ -46,7 +40,6 @@ 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.Status
import net.fortuna.ical4j.model.property.Summary
import net.fortuna.ical4j.util.TimeZones
import java.net.URI
import java.net.URISyntaxException
Expand Down Expand Up @@ -76,11 +69,22 @@ class LegacyAndroidEventProcessor(

private val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() }

private val fieldProcessors: Array<AndroidEventFieldProcessor> = arrayOf(
UidProcessor(),
AttendeesProcessor(),
RemindersProcessor(accountName)
)


fun populate(eventAndExceptions: EventAndExceptions, to: Event) {
populateEvent(eventAndExceptions.main, to = to)
populateEvent(
entity = eventAndExceptions.main,
main = eventAndExceptions.main,
to = to
)
populateExceptions(
exceptions = eventAndExceptions.exceptions,
main = eventAndExceptions.main,
originalAllDay = DateUtils.isDate(to.dtStart),
to = to
)
Expand All @@ -94,25 +98,25 @@ class LegacyAndroidEventProcessor(
* an [Event] data object.
*
* @param entity event row as returned by the calendar provider
* @param groupScheduled whether the event is group-scheduled (= the main event has attendees)
* @param main main event row as returned by the calendar provider
* @param to destination data object
*/
private fun populateEvent(entity: Entity, to: Event) {
// calculate some scheduling properties
private fun populateEvent(entity: Entity, main: Entity, to: Event) {
// legacy processors
val hasAttendees = entity.subValues.any { it.uri == Attendees.CONTENT_URI }

// main row
populateEventRow(entity.entityValues, groupScheduled = hasAttendees, to = to)

// data rows
for (subValue in entity.subValues) {
val subValues = subValue.values
when (subValue.uri) {
Attendees.CONTENT_URI -> populateAttendee(subValues, to = to)
Reminders.CONTENT_URI -> populateReminder(subValues, to = to)
ExtendedProperties.CONTENT_URI -> populateExtended(subValues, to = to)
}
}

// new processors
for (processor in fieldProcessors)
processor.process(from = entity, main = main, to = to)
}

private fun populateEventRow(row: ContentValues, groupScheduled: Boolean, to: Event) {
Expand Down Expand Up @@ -236,7 +240,6 @@ class LegacyAndroidEventProcessor(
logger.log(Level.WARNING, "Couldn't parse recurrence rules, ignoring", e)
}

to.uid = row.getAsString(Events.UID_2445)
to.sequence = row.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE)
to.isOrganizer = row.getAsBoolean(Events.IS_ORGANIZER)

Expand Down Expand Up @@ -306,79 +309,6 @@ class LegacyAndroidEventProcessor(
}
}

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

try {
val attendee: Attendee
val email = row.getAsString(Attendees.ATTENDEE_EMAIL)
val idNS = row.getAsString(Attendees.ATTENDEE_ID_NAMESPACE)
val id = row.getAsString(Attendees.ATTENDEE_IDENTITY)

if (idNS != null || id != null) {
// attendee identified by namespace and ID
attendee = Attendee(URI(idNS, id, null))
email?.let { attendee.parameters.add(Email(it)) }
} else
// attendee identified by email address
attendee = Attendee(URI("mailto", email, null))
val params = attendee.parameters

// always add RSVP (offer attendees to accept/decline)
params.add(Rsvp.TRUE)

row.getAsString(Attendees.ATTENDEE_NAME)?.let { cn -> params.add(Cn(cn)) }

// type/relation mapping is complex and thus outsourced to AttendeeMappings
AttendeeMappings.androidToICalendar(row, attendee)

// status
when (row.getAsInteger(Attendees.ATTENDEE_STATUS)) {
Attendees.ATTENDEE_STATUS_INVITED -> params.add(PartStat.NEEDS_ACTION)
Attendees.ATTENDEE_STATUS_ACCEPTED -> params.add(PartStat.ACCEPTED)
Attendees.ATTENDEE_STATUS_DECLINED -> params.add(PartStat.DECLINED)
Attendees.ATTENDEE_STATUS_TENTATIVE -> params.add(PartStat.TENTATIVE)
Attendees.ATTENDEE_STATUS_NONE -> { /* no information, don't add PARTSTAT */ }
}

to.attendees.add(attendee)
} catch (e: URISyntaxException) {
logger.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e)
}
}

private fun populateReminder(row: ContentValues, to: Event) {
logger.log(Level.FINE, "Read event reminder from calender provider", row)

val alarm = VAlarm(Duration.ofMinutes(-row.getAsLong(Reminders.MINUTES)))

val props = alarm.properties
when (row.getAsInteger(Reminders.METHOD)) {
Reminders.METHOD_EMAIL -> {
if (Patterns.EMAIL_ADDRESS.matcher(accountName).matches()) {
props += Action.EMAIL
// ACTION:EMAIL requires SUMMARY, DESCRIPTION, ATTENDEE
props += Summary(to.summary)
props += Description(to.description ?: to.summary)
// Android doesn't allow to save email reminder recipients, so we always use the
// account name (should be account owner's email address)
props += Attendee(URI("mailto", accountName, null))
} else {
logger.warning("Account name is not an email address; changing EMAIL reminder to DISPLAY")
props += Action.DISPLAY
props += Description(to.summary)
}
}

// default: set ACTION:DISPLAY (requires DESCRIPTION)
else -> {
props += Action.DISPLAY
props += Description(to.summary)
}
}
to.alarms += alarm
}

private fun populateExtended(row: ContentValues, to: Event) {
val name = row.getAsString(ExtendedProperties.NAME)
val rawValue = row.getAsString(ExtendedProperties.VALUE)
Expand All @@ -396,11 +326,6 @@ class LegacyAndroidEventProcessor(
logger.warning("Won't process invalid local URL: $rawValue")
}

AndroidEvent2.EXTNAME_ICAL_UID ->
// only consider iCalUid when there's no uid
if (to.uid == null)
to.uid = rawValue

UnknownProperty.CONTENT_ITEM_TYPE ->
to.unknownProperties += UnknownProperty.fromJsonString(rawValue)
}
Expand All @@ -409,12 +334,12 @@ class LegacyAndroidEventProcessor(
}
}

private fun populateExceptions(exceptions: List<Entity>, originalAllDay: Boolean, to: Event) {
private fun populateExceptions(exceptions: List<Entity>, main: Entity, originalAllDay: Boolean, to: Event) {
for (exception in exceptions) {
val exceptionEvent = Event()

// convert exception row to Event
populateEvent(exception, to = exceptionEvent)
populateEvent(exception, main, to = exceptionEvent)

// exceptions are required to have a RECURRENCE-ID
val recurrenceId = exceptionEvent.recurrenceId ?: continue
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.processor

import android.content.Entity
import at.bitfire.ical4android.Event

interface AndroidEventFieldProcessor {

/**
* Takes specific data from an event (= event row plus data rows, taken from the content provider)
* and maps it to the [Event] data class.
*
* If [from] references the same object as [main], this method is called for a main event (not an exception).
* If [from] references another object as [main], this method is called for an exception (not a main event).
*
* So you can use (note the referential equality operator):
*
* ```
* val isMainEvent = from === main // or
* val isException = from !== main
* ```
*
* In a later step of refactoring, it should map to [net.fortuna.ical4j.model.component.VEvent].
*
* @param from event from content provider
* @param main main event from content provider
* @param to destination object where the mapped data are stored
*/
fun process(from: Entity, main: Entity, to: Event)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.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.parameter.Cn
import net.fortuna.ical4j.model.parameter.Email
import net.fortuna.ical4j.model.parameter.PartStat
import net.fortuna.ical4j.model.parameter.Rsvp
import net.fortuna.ical4j.model.property.Attendee
import java.net.URI
import java.net.URISyntaxException
import java.util.logging.Level
import java.util.logging.Logger

class AttendeesProcessor: AndroidEventFieldProcessor {

private val logger
get() = Logger.getLogger(javaClass.name)

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

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

try {
val attendee: Attendee
val email = row.getAsString(Attendees.ATTENDEE_EMAIL)
val idNS = row.getAsString(Attendees.ATTENDEE_ID_NAMESPACE)
val id = row.getAsString(Attendees.ATTENDEE_IDENTITY)

if (idNS != null || id != null) {
// attendee identified by namespace and ID
attendee = Attendee(URI(idNS, id, null))
email?.let { attendee.parameters.add(Email(it)) }
} else
// attendee identified by email address
attendee = Attendee(URI("mailto", email, null))
val params = attendee.parameters

// always add RSVP (offer attendees to accept/decline)
params.add(Rsvp.TRUE)

row.getAsString(Attendees.ATTENDEE_NAME)?.let { cn -> params.add(Cn(cn)) }

// type/relation mapping is complex and thus outsourced to AttendeeMappings
AttendeeMappings.androidToICalendar(row, attendee)

// status
when (row.getAsInteger(Attendees.ATTENDEE_STATUS)) {
Attendees.ATTENDEE_STATUS_INVITED -> params.add(PartStat.NEEDS_ACTION)
Attendees.ATTENDEE_STATUS_ACCEPTED -> params.add(PartStat.ACCEPTED)
Attendees.ATTENDEE_STATUS_DECLINED -> params.add(PartStat.DECLINED)
Attendees.ATTENDEE_STATUS_TENTATIVE -> params.add(PartStat.TENTATIVE)
Attendees.ATTENDEE_STATUS_NONE -> { /* no information, don't add PARTSTAT */ }
}

to.attendees.add(attendee)
} catch (e: URISyntaxException) {
logger.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e)
}
}

}
Loading