Skip to content

Commit 1de710c

Browse files
authored
Event processor: create processor interface (#76)
* Add AttendeesProcessor * Add UidProcessor * Add RemindersProcessor * Fix typo * Also provide main Entity to processors * Indenting * Fix processor calls + tests
1 parent 3406c06 commit 1de710c

File tree

9 files changed

+740
-457
lines changed

9 files changed

+740
-457
lines changed

lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessorTest.kt

Lines changed: 0 additions & 357 deletions
Large diffs are not rendered by default.

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessor.kt

Lines changed: 25 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import android.content.Entity
1111
import android.provider.CalendarContract.Attendees
1212
import android.provider.CalendarContract.Events
1313
import android.provider.CalendarContract.ExtendedProperties
14-
import android.provider.CalendarContract.Reminders
15-
import android.util.Patterns
1614
import at.bitfire.ical4android.Event
1715
import at.bitfire.ical4android.UnknownProperty
1816
import at.bitfire.ical4android.util.AndroidTimeUtils
@@ -21,22 +19,18 @@ import at.bitfire.ical4android.util.TimeApiExtensions
2119
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
2220
import at.bitfire.synctools.exception.InvalidLocalResourceException
2321
import at.bitfire.synctools.icalendar.Css3Color
22+
import at.bitfire.synctools.mapping.calendar.processor.AndroidEventFieldProcessor
23+
import at.bitfire.synctools.mapping.calendar.processor.AttendeesProcessor
24+
import at.bitfire.synctools.mapping.calendar.processor.RemindersProcessor
25+
import at.bitfire.synctools.mapping.calendar.processor.UidProcessor
2426
import at.bitfire.synctools.storage.calendar.AndroidEvent2
2527
import at.bitfire.synctools.storage.calendar.EventAndExceptions
2628
import net.fortuna.ical4j.model.Date
2729
import net.fortuna.ical4j.model.DateList
2830
import net.fortuna.ical4j.model.DateTime
2931
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
30-
import net.fortuna.ical4j.model.component.VAlarm
31-
import net.fortuna.ical4j.model.parameter.Cn
32-
import net.fortuna.ical4j.model.parameter.Email
33-
import net.fortuna.ical4j.model.parameter.PartStat
34-
import net.fortuna.ical4j.model.parameter.Rsvp
3532
import net.fortuna.ical4j.model.parameter.Value
36-
import net.fortuna.ical4j.model.property.Action
37-
import net.fortuna.ical4j.model.property.Attendee
3833
import net.fortuna.ical4j.model.property.Clazz
39-
import net.fortuna.ical4j.model.property.Description
4034
import net.fortuna.ical4j.model.property.DtEnd
4135
import net.fortuna.ical4j.model.property.DtStart
4236
import net.fortuna.ical4j.model.property.ExDate
@@ -46,7 +40,6 @@ import net.fortuna.ical4j.model.property.RDate
4640
import net.fortuna.ical4j.model.property.RRule
4741
import net.fortuna.ical4j.model.property.RecurrenceId
4842
import net.fortuna.ical4j.model.property.Status
49-
import net.fortuna.ical4j.model.property.Summary
5043
import net.fortuna.ical4j.util.TimeZones
5144
import java.net.URI
5245
import java.net.URISyntaxException
@@ -76,11 +69,22 @@ class LegacyAndroidEventProcessor(
7669

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

72+
private val fieldProcessors: Array<AndroidEventFieldProcessor> = arrayOf(
73+
UidProcessor(),
74+
AttendeesProcessor(),
75+
RemindersProcessor(accountName)
76+
)
77+
7978

8079
fun populate(eventAndExceptions: EventAndExceptions, to: Event) {
81-
populateEvent(eventAndExceptions.main, to = to)
80+
populateEvent(
81+
entity = eventAndExceptions.main,
82+
main = eventAndExceptions.main,
83+
to = to
84+
)
8285
populateExceptions(
8386
exceptions = eventAndExceptions.exceptions,
87+
main = eventAndExceptions.main,
8488
originalAllDay = DateUtils.isDate(to.dtStart),
8589
to = to
8690
)
@@ -94,25 +98,25 @@ class LegacyAndroidEventProcessor(
9498
* an [Event] data object.
9599
*
96100
* @param entity event row as returned by the calendar provider
97-
* @param groupScheduled whether the event is group-scheduled (= the main event has attendees)
101+
* @param main main event row as returned by the calendar provider
98102
* @param to destination data object
99103
*/
100-
private fun populateEvent(entity: Entity, to: Event) {
101-
// calculate some scheduling properties
104+
private fun populateEvent(entity: Entity, main: Entity, to: Event) {
105+
// legacy processors
102106
val hasAttendees = entity.subValues.any { it.uri == Attendees.CONTENT_URI }
103-
104-
// main row
105107
populateEventRow(entity.entityValues, groupScheduled = hasAttendees, to = to)
106108

107109
// data rows
108110
for (subValue in entity.subValues) {
109111
val subValues = subValue.values
110112
when (subValue.uri) {
111-
Attendees.CONTENT_URI -> populateAttendee(subValues, to = to)
112-
Reminders.CONTENT_URI -> populateReminder(subValues, to = to)
113113
ExtendedProperties.CONTENT_URI -> populateExtended(subValues, to = to)
114114
}
115115
}
116+
117+
// new processors
118+
for (processor in fieldProcessors)
119+
processor.process(from = entity, main = main, to = to)
116120
}
117121

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

239-
to.uid = row.getAsString(Events.UID_2445)
240243
to.sequence = row.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE)
241244
to.isOrganizer = row.getAsBoolean(Events.IS_ORGANIZER)
242245

@@ -306,79 +309,6 @@ class LegacyAndroidEventProcessor(
306309
}
307310
}
308311

309-
private fun populateAttendee(row: ContentValues, to: Event) {
310-
logger.log(Level.FINE, "Read event attendee from calender provider", row)
311-
312-
try {
313-
val attendee: Attendee
314-
val email = row.getAsString(Attendees.ATTENDEE_EMAIL)
315-
val idNS = row.getAsString(Attendees.ATTENDEE_ID_NAMESPACE)
316-
val id = row.getAsString(Attendees.ATTENDEE_IDENTITY)
317-
318-
if (idNS != null || id != null) {
319-
// attendee identified by namespace and ID
320-
attendee = Attendee(URI(idNS, id, null))
321-
email?.let { attendee.parameters.add(Email(it)) }
322-
} else
323-
// attendee identified by email address
324-
attendee = Attendee(URI("mailto", email, null))
325-
val params = attendee.parameters
326-
327-
// always add RSVP (offer attendees to accept/decline)
328-
params.add(Rsvp.TRUE)
329-
330-
row.getAsString(Attendees.ATTENDEE_NAME)?.let { cn -> params.add(Cn(cn)) }
331-
332-
// type/relation mapping is complex and thus outsourced to AttendeeMappings
333-
AttendeeMappings.androidToICalendar(row, attendee)
334-
335-
// status
336-
when (row.getAsInteger(Attendees.ATTENDEE_STATUS)) {
337-
Attendees.ATTENDEE_STATUS_INVITED -> params.add(PartStat.NEEDS_ACTION)
338-
Attendees.ATTENDEE_STATUS_ACCEPTED -> params.add(PartStat.ACCEPTED)
339-
Attendees.ATTENDEE_STATUS_DECLINED -> params.add(PartStat.DECLINED)
340-
Attendees.ATTENDEE_STATUS_TENTATIVE -> params.add(PartStat.TENTATIVE)
341-
Attendees.ATTENDEE_STATUS_NONE -> { /* no information, don't add PARTSTAT */ }
342-
}
343-
344-
to.attendees.add(attendee)
345-
} catch (e: URISyntaxException) {
346-
logger.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e)
347-
}
348-
}
349-
350-
private fun populateReminder(row: ContentValues, to: Event) {
351-
logger.log(Level.FINE, "Read event reminder from calender provider", row)
352-
353-
val alarm = VAlarm(Duration.ofMinutes(-row.getAsLong(Reminders.MINUTES)))
354-
355-
val props = alarm.properties
356-
when (row.getAsInteger(Reminders.METHOD)) {
357-
Reminders.METHOD_EMAIL -> {
358-
if (Patterns.EMAIL_ADDRESS.matcher(accountName).matches()) {
359-
props += Action.EMAIL
360-
// ACTION:EMAIL requires SUMMARY, DESCRIPTION, ATTENDEE
361-
props += Summary(to.summary)
362-
props += Description(to.description ?: to.summary)
363-
// Android doesn't allow to save email reminder recipients, so we always use the
364-
// account name (should be account owner's email address)
365-
props += Attendee(URI("mailto", accountName, null))
366-
} else {
367-
logger.warning("Account name is not an email address; changing EMAIL reminder to DISPLAY")
368-
props += Action.DISPLAY
369-
props += Description(to.summary)
370-
}
371-
}
372-
373-
// default: set ACTION:DISPLAY (requires DESCRIPTION)
374-
else -> {
375-
props += Action.DISPLAY
376-
props += Description(to.summary)
377-
}
378-
}
379-
to.alarms += alarm
380-
}
381-
382312
private fun populateExtended(row: ContentValues, to: Event) {
383313
val name = row.getAsString(ExtendedProperties.NAME)
384314
val rawValue = row.getAsString(ExtendedProperties.VALUE)
@@ -396,11 +326,6 @@ class LegacyAndroidEventProcessor(
396326
logger.warning("Won't process invalid local URL: $rawValue")
397327
}
398328

399-
AndroidEvent2.EXTNAME_ICAL_UID ->
400-
// only consider iCalUid when there's no uid
401-
if (to.uid == null)
402-
to.uid = rawValue
403-
404329
UnknownProperty.CONTENT_ITEM_TYPE ->
405330
to.unknownProperties += UnknownProperty.fromJsonString(rawValue)
406331
}
@@ -409,12 +334,12 @@ class LegacyAndroidEventProcessor(
409334
}
410335
}
411336

412-
private fun populateExceptions(exceptions: List<Entity>, originalAllDay: Boolean, to: Event) {
337+
private fun populateExceptions(exceptions: List<Entity>, main: Entity, originalAllDay: Boolean, to: Event) {
413338
for (exception in exceptions) {
414339
val exceptionEvent = Event()
415340

416341
// convert exception row to Event
417-
populateEvent(exception, to = exceptionEvent)
342+
populateEvent(exception, main, to = exceptionEvent)
418343

419344
// exceptions are required to have a RECURRENCE-ID
420345
val recurrenceId = exceptionEvent.recurrenceId ?: continue
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.calendar.processor
8+
9+
import android.content.Entity
10+
import at.bitfire.ical4android.Event
11+
12+
interface AndroidEventFieldProcessor {
13+
14+
/**
15+
* Takes specific data from an event (= event row plus data rows, taken from the content provider)
16+
* and maps it to the [Event] data class.
17+
*
18+
* If [from] references the same object as [main], this method is called for a main event (not an exception).
19+
* If [from] references another object as [main], this method is called for an exception (not a main event).
20+
*
21+
* So you can use (note the referential equality operator):
22+
*
23+
* ```
24+
* val isMainEvent = from === main // or
25+
* val isException = from !== main
26+
* ```
27+
*
28+
* In a later step of refactoring, it should map to [net.fortuna.ical4j.model.component.VEvent].
29+
*
30+
* @param from event from content provider
31+
* @param main main event from content provider
32+
* @param to destination object where the mapped data are stored
33+
*/
34+
fun process(from: Entity, main: Entity, to: Event)
35+
36+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.mapping.calendar.processor
8+
9+
import android.content.ContentValues
10+
import android.content.Entity
11+
import android.provider.CalendarContract.Attendees
12+
import at.bitfire.ical4android.Event
13+
import at.bitfire.synctools.mapping.calendar.AttendeeMappings
14+
import net.fortuna.ical4j.model.parameter.Cn
15+
import net.fortuna.ical4j.model.parameter.Email
16+
import net.fortuna.ical4j.model.parameter.PartStat
17+
import net.fortuna.ical4j.model.parameter.Rsvp
18+
import net.fortuna.ical4j.model.property.Attendee
19+
import java.net.URI
20+
import java.net.URISyntaxException
21+
import java.util.logging.Level
22+
import java.util.logging.Logger
23+
24+
class AttendeesProcessor: AndroidEventFieldProcessor {
25+
26+
private val logger
27+
get() = Logger.getLogger(javaClass.name)
28+
29+
override fun process(from: Entity, main: Entity, to: Event) {
30+
for (row in from.subValues.filter { it.uri == Attendees.CONTENT_URI })
31+
populateAttendee(row.values, to)
32+
}
33+
34+
private fun populateAttendee(row: ContentValues, to: Event) {
35+
logger.log(Level.FINE, "Read event attendee from calendar provider", row)
36+
37+
try {
38+
val attendee: Attendee
39+
val email = row.getAsString(Attendees.ATTENDEE_EMAIL)
40+
val idNS = row.getAsString(Attendees.ATTENDEE_ID_NAMESPACE)
41+
val id = row.getAsString(Attendees.ATTENDEE_IDENTITY)
42+
43+
if (idNS != null || id != null) {
44+
// attendee identified by namespace and ID
45+
attendee = Attendee(URI(idNS, id, null))
46+
email?.let { attendee.parameters.add(Email(it)) }
47+
} else
48+
// attendee identified by email address
49+
attendee = Attendee(URI("mailto", email, null))
50+
val params = attendee.parameters
51+
52+
// always add RSVP (offer attendees to accept/decline)
53+
params.add(Rsvp.TRUE)
54+
55+
row.getAsString(Attendees.ATTENDEE_NAME)?.let { cn -> params.add(Cn(cn)) }
56+
57+
// type/relation mapping is complex and thus outsourced to AttendeeMappings
58+
AttendeeMappings.androidToICalendar(row, attendee)
59+
60+
// status
61+
when (row.getAsInteger(Attendees.ATTENDEE_STATUS)) {
62+
Attendees.ATTENDEE_STATUS_INVITED -> params.add(PartStat.NEEDS_ACTION)
63+
Attendees.ATTENDEE_STATUS_ACCEPTED -> params.add(PartStat.ACCEPTED)
64+
Attendees.ATTENDEE_STATUS_DECLINED -> params.add(PartStat.DECLINED)
65+
Attendees.ATTENDEE_STATUS_TENTATIVE -> params.add(PartStat.TENTATIVE)
66+
Attendees.ATTENDEE_STATUS_NONE -> { /* no information, don't add PARTSTAT */ }
67+
}
68+
69+
to.attendees.add(attendee)
70+
} catch (e: URISyntaxException) {
71+
logger.log(Level.WARNING, "Couldn't parse attendee information, ignoring", e)
72+
}
73+
}
74+
75+
}

0 commit comments

Comments
 (0)