Skip to content

Commit 2383958

Browse files
authored
Event builders: use VEvent instead of deprecated Event as input (#120)
* [WIP] Rewrite AndroidEventBuilder to use VEvent * [WIP] Continue * [WIP] Rewrite Builders * [WIP] Rewrite tests * [WIP] Fix remaining tests (UnknownPropertiesBuilder yet not working) * Mark EventReader as deprecated, move util method * Update UnknownPropertiesBuilder and Processor - Add unknownProperties() method to filter known properties - Update test for unknown properties handling - Refactor KNOWN_PROPERTY_NAMES to include processed properties * AndroidEventBuilder: document how main VEvent is generated when it's missing
1 parent 05fb1e2 commit 2383958

File tree

62 files changed

+791
-662
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+791
-662
lines changed

lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,13 @@ abstract class DmfsTask(
388388
protected open fun insertAlarms(batch: TasksBatchOperation, idxTask: Int?) {
389389
val task = requireNotNull(task)
390390
for (alarm in task.alarms) {
391-
val (alarmRef, minutes) = ICalendar.vAlarmToMin(alarm, task, true) ?: continue
391+
val (alarmRef, minutes) = ICalendar.vAlarmToMin(
392+
alarm = alarm,
393+
refStart = task.dtStart,
394+
refEnd = task.due,
395+
refDuration = task.duration,
396+
allowRelEnd = true
397+
) ?: continue
392398
val ref = when (alarmRef) {
393399
Related.END ->
394400
Alarm.ALARM_REFERENCE_DUE_DATE

lib/src/main/kotlin/at/bitfire/ical4android/Event.kt

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ package at.bitfire.ical4android
88

99
import at.bitfire.synctools.exception.InvalidICalendarException
1010
import at.bitfire.synctools.icalendar.Css3Color
11-
import net.fortuna.ical4j.model.Parameter
1211
import net.fortuna.ical4j.model.Property
1312
import net.fortuna.ical4j.model.component.VAlarm
14-
import net.fortuna.ical4j.model.parameter.Email
1513
import net.fortuna.ical4j.model.property.Attendee
1614
import net.fortuna.ical4j.model.property.Clazz
1715
import net.fortuna.ical4j.model.property.DtEnd
@@ -34,7 +32,11 @@ import java.util.LinkedList
3432
* - as it is extracted from an iCalendar or
3533
* - as it should be generated into an iCalendar.
3634
*/
37-
@Deprecated("Use AssociatedEvents instead", replaceWith = ReplaceWith("AssociatedEvents", "at.bitfire.synctools.icalendar"))
35+
@Deprecated(
36+
"Use AssociatedEvents instead",
37+
replaceWith = ReplaceWith("AssociatedEvents", "at.bitfire.synctools.icalendar"),
38+
level = DeprecationLevel.WARNING
39+
)
3840
data class Event(
3941
override var uid: String? = null,
4042
override var sequence: Int? = null,
@@ -79,19 +81,6 @@ data class Event(
7981
val unknownProperties: LinkedList<Property> = LinkedList()
8082
) : ICalendar() {
8183

82-
val organizerEmail: String?
83-
get() {
84-
var email: String? = null
85-
organizer?.let { organizer ->
86-
val uri = organizer.calAddress
87-
email = if (uri.scheme.equals("mailto", true))
88-
uri.schemeSpecificPart
89-
else
90-
organizer.getParameter<Email>(Parameter.EMAIL)?.value
91-
}
92-
return email
93-
}
94-
9584
fun requireDtStart(): DtStart =
9685
dtStart ?: throw InvalidICalendarException("Missing DTSTART in VEVENT")
9786

lib/src/main/kotlin/at/bitfire/ical4android/EventReader.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import java.util.logging.Logger
4545
/**
4646
* Generates an [Event] from an iCalendar in a [Reader] source.
4747
*/
48+
@Deprecated("Use ICalendarParser and CalendarUidSplitter instead")
4849
class EventReader {
4950

5051
private val logger

lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import net.fortuna.ical4j.model.component.Daylight
2323
import net.fortuna.ical4j.model.component.Observance
2424
import net.fortuna.ical4j.model.component.Standard
2525
import net.fortuna.ical4j.model.component.VAlarm
26-
import net.fortuna.ical4j.model.component.VEvent
2726
import net.fortuna.ical4j.model.component.VTimeZone
28-
import net.fortuna.ical4j.model.component.VToDo
2927
import net.fortuna.ical4j.model.parameter.Related
3028
import net.fortuna.ical4j.model.property.Color
29+
import net.fortuna.ical4j.model.property.DateProperty
30+
import net.fortuna.ical4j.model.property.DtStart
3131
import net.fortuna.ical4j.model.property.ProdId
3232
import net.fortuna.ical4j.model.property.RDate
3333
import net.fortuna.ical4j.model.property.RRule
@@ -261,9 +261,10 @@ open class ICalendar {
261261
/**
262262
* Calculates the minutes before/after an event/task a given alarm occurs.
263263
*
264-
* @param alarm the alarm to calculate the minutes from
265-
* @param reference reference [VEvent] or [VToDo] to take start/end time from (required for calculations)
266-
* @param allowRelEnd *true*: caller accepts minutes related to the end;
264+
* @param alarm the alarm to calculate the minutes from
265+
* @param refStart reference `DTSTART` from the calendar component
266+
* @param refEnd reference `DTEND` (`VEVENT`) or `DUE` (`VTODO`) from the calendar component
267+
* @param allowRelEnd *true*: caller accepts minutes related to the end;
267268
* *false*: caller only accepts minutes related to the start
268269
*
269270
* Android's alarm granularity is minutes. This methods calculates with milliseconds, but the result
@@ -276,44 +277,35 @@ open class ICalendar {
276277
*
277278
* May be *null* if there's not enough information to calculate the number of minutes.
278279
*/
279-
fun vAlarmToMin(alarm: VAlarm, reference: ICalendar, allowRelEnd: Boolean): Pair<Related, Int>? {
280+
fun vAlarmToMin(
281+
alarm: VAlarm,
282+
refStart: DtStart?,
283+
refEnd: DateProperty?,
284+
refDuration: net.fortuna.ical4j.model.property.Duration?,
285+
allowRelEnd: Boolean
286+
): Pair<Related, Int>? {
280287
val trigger = alarm.trigger ?: return null
281288

282289
val minutes: Int // minutes before/after the event
283290
var related = trigger.getParameter<Related>(Parameter.RELATED) ?: Related.START
284291

285292
// event/task start time
286-
val start: java.util.Date?
287-
var end: java.util.Date?
288-
when (reference) {
289-
is Event -> {
290-
start = reference.dtStart?.date
291-
end = reference.dtEnd?.date
292-
}
293-
is Task -> {
294-
start = reference.dtStart?.date
295-
end = reference.due?.date
296-
}
297-
else -> throw IllegalArgumentException("reference must be Event or Task")
298-
}
293+
val start: java.util.Date? = refStart?.date
294+
var end: java.util.Date? = refEnd?.date
299295

300296
// event/task end time
301297
if (end == null && start != null) {
302-
val duration = when (reference) {
303-
is Event -> reference.duration?.duration
304-
is Task -> reference.duration?.duration
305-
else -> throw IllegalArgumentException("reference must be Event or Task")
306-
}
298+
val duration = refDuration?.duration
307299
if (duration != null)
308300
end = java.util.Date.from(start.toInstant() + duration)
309301
}
310302

311303
// event/task duration
312304
val duration: Duration? =
313-
if (start != null && end != null)
314-
Duration.between(start.toInstant(), end.toInstant())
315-
else
316-
null
305+
if (start != null && end != null)
306+
Duration.between(start.toInstant(), end.toInstant())
307+
else
308+
null
317309

318310
val triggerDur = trigger.duration
319311
val triggerTime = trigger.dateTime

lib/src/main/kotlin/at/bitfire/synctools/icalendar/Ical4jHelpers.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
package at.bitfire.synctools.icalendar
88

99
import at.bitfire.synctools.BuildConfig
10+
import at.bitfire.synctools.exception.InvalidICalendarException
1011
import net.fortuna.ical4j.model.ComponentList
1112
import net.fortuna.ical4j.model.Property
1213
import net.fortuna.ical4j.model.PropertyList
1314
import net.fortuna.ical4j.model.component.CalendarComponent
15+
import net.fortuna.ical4j.model.component.VEvent
16+
import net.fortuna.ical4j.model.property.DtStart
1417
import net.fortuna.ical4j.model.property.RecurrenceId
1518
import net.fortuna.ical4j.model.property.Sequence
1619
import net.fortuna.ical4j.model.property.Uid
@@ -42,3 +45,6 @@ val CalendarComponent.recurrenceId: RecurrenceId?
4245

4346
val CalendarComponent.sequence: Sequence?
4447
get() = getProperty(Property.SEQUENCE)
48+
49+
fun VEvent.requireDtStart(): DtStart =
50+
startDate ?: throw InvalidICalendarException("Missing DTSTART in VEVENT")

lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventBuilder2.kt renamed to lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventBuilder.kt

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ package at.bitfire.synctools.mapping.calendar
88

99
import android.content.ContentValues
1010
import android.content.Entity
11-
import at.bitfire.ical4android.Event
11+
import at.bitfire.synctools.icalendar.AssociatedEvents
1212
import at.bitfire.synctools.mapping.calendar.builder.AccessLevelBuilder
1313
import at.bitfire.synctools.mapping.calendar.builder.AllDayBuilder
1414
import at.bitfire.synctools.mapping.calendar.builder.AndroidEntityBuilder
@@ -38,20 +38,17 @@ import at.bitfire.synctools.mapping.calendar.builder.UnknownPropertiesBuilder
3838
import at.bitfire.synctools.mapping.calendar.builder.UrlBuilder
3939
import at.bitfire.synctools.storage.calendar.AndroidCalendar
4040
import at.bitfire.synctools.storage.calendar.EventAndExceptions
41+
import net.fortuna.ical4j.model.component.VEvent
4142

4243
/**
43-
* Legacy mapper from an [Event] data object to Android content provider data rows
44+
* Legacy mapper from an [AssociatedEvents] data object to Android content provider data rows
4445
* (former "build..." methods).
4546
*
4647
* Important: To use recurrence exceptions, you MUST set _SYNC_ID and ORIGINAL_SYNC_ID
4748
* in populateEvent() / buildEvent. Setting _ID and ORIGINAL_ID is not sufficient.
48-
*
49-
* Note: "Legacy" will be removed from the class name as soon as the [Event] dependency is
50-
* replaced by [at.bitfire.synctools.icalendar.AssociatedEvents].
5149
*/
52-
class LegacyAndroidEventBuilder2(
50+
class AndroidEventBuilder(
5351
calendar: AndroidCalendar,
54-
private val event: Event,
5552

5653
// AndroidEvent-level fields
5754
syncId: String?,
@@ -92,19 +89,37 @@ class LegacyAndroidEventBuilder2(
9289
UrlBuilder()
9390
)
9491

95-
fun build() =
96-
EventAndExceptions(
97-
main = buildEvent(null),
98-
exceptions = event.exceptions.map { exception ->
99-
buildEvent(exception)
92+
fun build(events: AssociatedEvents): EventAndExceptions {
93+
val mainVEvent = events.main ?: createMainFromExceptions(events.exceptions)
94+
return EventAndExceptions(
95+
main = buildEvent(from = mainVEvent, main = mainVEvent),
96+
exceptions = events.exceptions.map { exception ->
97+
buildEvent(from = exception, main = mainVEvent)
10098
}
10199
)
100+
}
102101

103-
fun buildEvent(recurrence: Event?): Entity {
102+
fun buildEvent(from: VEvent, main: VEvent): Entity {
104103
val entity = Entity(ContentValues())
105104
for (builder in fieldBuilders)
106-
builder.build(from = recurrence ?: event, main = event, to = entity)
105+
builder.build(from = from, main = main, to = entity)
107106
return entity
108107
}
109108

109+
/**
110+
* It is possible that a user receives only exceptions of an event, but not the main event itself.
111+
* This happens when there's a recurring event that is not visible for the user, but the user is invited to
112+
* a single recurrence. However, we always need a main event for Android, so we make up one from the
113+
* exceptions.
114+
*/
115+
private fun createMainFromExceptions(exceptions: List<VEvent>): VEvent {
116+
// Should in the future be replaced by a real event that has a title like "(unknown event)".
117+
// This main event should also have a special extended property that indicates that the event
118+
// must not actually be generated as main VEvent when the event is locally edited and then uploaded.
119+
120+
// Currently, we just use the first exception as a main event, too. This is not correct and
121+
// should be fixed.
122+
return exceptions.firstOrNull() ?: VEvent()
123+
}
124+
110125
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import android.content.Entity
1010
import android.provider.CalendarContract.Events
1111
import android.provider.CalendarContract.ExtendedProperties
1212
import androidx.core.content.contentValuesOf
13-
import at.bitfire.ical4android.Event
1413
import at.bitfire.ical4android.UnknownProperty
14+
import net.fortuna.ical4j.model.component.VEvent
1515
import net.fortuna.ical4j.model.property.Clazz
1616

1717
class AccessLevelBuilder: AndroidEntityBuilder {
1818

19-
override fun build(from: Event, main: Event, to: Entity) {
19+
override fun build(from: VEvent, main: VEvent, to: Entity) {
2020
val accessLevel: Int
2121
val retainValue: Boolean
2222

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ package at.bitfire.synctools.mapping.calendar.builder
88

99
import android.content.Entity
1010
import android.provider.CalendarContract.Events
11-
import at.bitfire.ical4android.Event
1211
import at.bitfire.ical4android.util.DateUtils
12+
import net.fortuna.ical4j.model.component.VEvent
1313

1414
class AllDayBuilder: AndroidEntityBuilder {
1515

16-
override fun build(from: Event, main: Event, to: Entity) {
17-
val allDay = DateUtils.isDate(from.dtStart)
16+
override fun build(from: VEvent, main: VEvent, to: Entity) {
17+
val allDay = DateUtils.isDate(from.startDate)
1818
to.entityValues.put(Events.ALL_DAY, if (allDay) 1 else 0)
1919
}
2020

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
package at.bitfire.synctools.mapping.calendar.builder
88

99
import android.content.Entity
10-
import at.bitfire.ical4android.Event
10+
import net.fortuna.ical4j.model.component.VEvent
1111

1212
interface AndroidEntityBuilder {
1313

@@ -34,6 +34,6 @@ interface AndroidEntityBuilder {
3434
*
3535
* @throws at.bitfire.synctools.exception.InvalidICalendarException on missing or invalid required properties (like DTSTART)
3636
*/
37-
fun build(from: Event, main: Event, to: Entity)
37+
fun build(from: VEvent, main: VEvent, to: Entity)
3838

3939
}

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ package at.bitfire.synctools.mapping.calendar.builder
99
import android.content.ContentValues
1010
import android.content.Entity
1111
import android.provider.CalendarContract.Attendees
12-
import at.bitfire.ical4android.Event
12+
import androidx.annotation.VisibleForTesting
1313
import at.bitfire.synctools.mapping.calendar.AttendeeMappings
1414
import at.bitfire.synctools.storage.calendar.AndroidCalendar
1515
import net.fortuna.ical4j.model.Parameter
16+
import net.fortuna.ical4j.model.Property
17+
import net.fortuna.ical4j.model.component.VEvent
1618
import net.fortuna.ical4j.model.parameter.Cn
1719
import net.fortuna.ical4j.model.parameter.Email
1820
import net.fortuna.ical4j.model.parameter.PartStat
@@ -22,14 +24,14 @@ class AttendeesBuilder(
2224
private val calendar: AndroidCalendar
2325
): AndroidEntityBuilder {
2426

25-
override fun build(from: Event, main: Event, to: Entity) {
26-
for (attendee in from.attendees)
27+
override fun build(from: VEvent, main: VEvent, to: Entity) {
28+
for (attendee in from.getProperties<Attendee>(Property.ATTENDEE))
2729
to.addSubValue(Attendees.CONTENT_URI, buildAttendee(attendee, from))
2830
}
2931

30-
private fun buildAttendee(attendee: Attendee, event: Event): ContentValues {
32+
private fun buildAttendee(attendee: Attendee, event: VEvent): ContentValues {
3133
val values = ContentValues()
32-
val organizer = event.organizerEmail ?:
34+
val organizer = organizerEmail(event) ?:
3335
/* no ORGANIZER, use current account owner as ORGANIZER */
3436
calendar.ownerAccount ?: calendar.account.name
3537

@@ -65,4 +67,16 @@ class AttendeesBuilder(
6567
return values
6668
}
6769

70+
@VisibleForTesting
71+
internal fun organizerEmail(event: VEvent): String? {
72+
event.organizer?.let { organizer ->
73+
val uri = organizer.calAddress
74+
return if (uri.scheme.equals("mailto", true))
75+
uri.schemeSpecificPart
76+
else
77+
organizer.getParameter<Email>(Parameter.EMAIL)?.value
78+
}
79+
return null
80+
}
81+
6882
}

0 commit comments

Comments
 (0)