Skip to content

Commit 38917d8

Browse files
authored
Migrate event sub-row builders (#67)
* Text fields * Migrate builders for `CATEGORIES`, `URL`, retained `CLASS` and unknown properties * Remove obsolete tests * Migrate builders for sub-rows * Fix test
1 parent 1de710c commit 38917d8

20 files changed

+1492
-737
lines changed

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

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

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

Lines changed: 27 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,10 @@ package at.bitfire.synctools.mapping.calendar
88

99
import android.content.ContentValues
1010
import android.content.Entity
11-
import android.provider.CalendarContract.Attendees
1211
import android.provider.CalendarContract.Colors
1312
import android.provider.CalendarContract.Events
14-
import android.provider.CalendarContract.ExtendedProperties
15-
import android.provider.CalendarContract.Reminders
1613
import androidx.core.content.contentValuesOf
1714
import at.bitfire.ical4android.Event
18-
import at.bitfire.ical4android.ICalendar
19-
import at.bitfire.ical4android.UnknownProperty
2015
import at.bitfire.ical4android.util.AndroidTimeUtils
2116
import at.bitfire.ical4android.util.DateUtils
2217
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
@@ -28,31 +23,32 @@ import at.bitfire.ical4android.util.TimeApiExtensions.toLocalTime
2823
import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration
2924
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
3025
import at.bitfire.synctools.exception.InvalidLocalResourceException
31-
import at.bitfire.synctools.mapping.calendar.builder.AndroidEventFieldBuilder
26+
import at.bitfire.synctools.mapping.calendar.builder.AndroidEntityBuilder
27+
import at.bitfire.synctools.mapping.calendar.builder.AttendeesBuilder
28+
import at.bitfire.synctools.mapping.calendar.builder.CategoriesBuilder
29+
import at.bitfire.synctools.mapping.calendar.builder.DescriptionBuilder
30+
import at.bitfire.synctools.mapping.calendar.builder.LocationBuilder
31+
import at.bitfire.synctools.mapping.calendar.builder.RemindersBuilder
32+
import at.bitfire.synctools.mapping.calendar.builder.RetainedClassificationBuilder
3233
import at.bitfire.synctools.mapping.calendar.builder.TitleBuilder
34+
import at.bitfire.synctools.mapping.calendar.builder.UnknownPropertiesBuilder
35+
import at.bitfire.synctools.mapping.calendar.builder.UrlBuilder
3336
import at.bitfire.synctools.storage.calendar.AndroidCalendar
3437
import at.bitfire.synctools.storage.calendar.AndroidEvent2
3538
import at.bitfire.synctools.storage.calendar.EventAndExceptions
3639
import net.fortuna.ical4j.model.Date
3740
import net.fortuna.ical4j.model.DateList
3841
import net.fortuna.ical4j.model.DateTime
3942
import net.fortuna.ical4j.model.Parameter
40-
import net.fortuna.ical4j.model.Property
4143
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
42-
import net.fortuna.ical4j.model.component.VAlarm
43-
import net.fortuna.ical4j.model.parameter.Cn
4444
import net.fortuna.ical4j.model.parameter.Email
45-
import net.fortuna.ical4j.model.parameter.PartStat
46-
import net.fortuna.ical4j.model.property.Action
47-
import net.fortuna.ical4j.model.property.Attendee
4845
import net.fortuna.ical4j.model.property.Clazz
4946
import net.fortuna.ical4j.model.property.DtEnd
5047
import net.fortuna.ical4j.model.property.RDate
5148
import net.fortuna.ical4j.model.property.Status
5249
import java.time.Duration
5350
import java.time.Period
5451
import java.time.ZonedDateTime
55-
import java.util.Locale
5652
import java.util.logging.Logger
5753

5854
/**
@@ -77,6 +73,20 @@ class LegacyAndroidEventBuilder2(
7773
private val flags: Int
7874
) {
7975

76+
private val fieldBuilders: Array<AndroidEntityBuilder> = arrayOf(
77+
// event fields (order as in CalendarContract.EventsColumns)
78+
TitleBuilder(),
79+
DescriptionBuilder(),
80+
LocationBuilder(),
81+
// sub-rows (alphabetically, by class name)
82+
AttendeesBuilder(calendar),
83+
CategoriesBuilder(),
84+
RemindersBuilder(),
85+
RetainedClassificationBuilder(),
86+
UnknownPropertiesBuilder(),
87+
UrlBuilder()
88+
)
89+
8090
private val logger
8191
get() = Logger.getLogger(javaClass.name)
8292

@@ -92,43 +102,13 @@ class LegacyAndroidEventBuilder2(
92102
)
93103

94104
fun buildEvent(recurrence: Event?): Entity {
105+
// build main row from legacy builders
95106
val row = buildEventRow(recurrence)
96107

108+
// additionally apply new builders
97109
val entity = Entity(row)
98-
val from = recurrence ?: event
99-
100-
// new builders
101-
102-
for (builder in fieldBuilders())
103-
builder.build(from = from, main = event, to = entity)
104-
105-
// legacy fields
106-
107-
for (reminder in from.alarms)
108-
entity.addSubValue(Reminders.CONTENT_URI, buildReminder(reminder))
109-
110-
for (attendee in from.attendees)
111-
entity.addSubValue(Attendees.CONTENT_URI, buildAttendee(attendee))
112-
113-
// extended properties
114-
if (event.categories.isNotEmpty())
115-
entity.addSubValue(ExtendedProperties.CONTENT_URI, buildCategories(event.categories))
116-
117-
event.classification?.let { classification ->
118-
val values = buildRetainedClassification(classification)
119-
if (values != null)
120-
entity.addSubValue(ExtendedProperties.CONTENT_URI, values)
121-
}
122-
123-
event.url?.let { url ->
124-
entity.addSubValue(ExtendedProperties.CONTENT_URI, buildUrl(url.toString()))
125-
}
126-
127-
for (unknownProperty in event.unknownProperties) {
128-
val values = buildUnknownProperty(unknownProperty)
129-
if (values != null)
130-
entity.addSubValue(ExtendedProperties.CONTENT_URI, values)
131-
}
110+
for (builder in fieldBuilders)
111+
builder.build(from = recurrence ?: event, main = event, to = entity)
132112

133113
return entity
134114
}
@@ -339,10 +319,6 @@ class LegacyAndroidEventBuilder2(
339319
row.putNull(Events.EXDATE)
340320
}
341321

342-
// text fields
343-
row.put(Events.EVENT_LOCATION, from.location)
344-
row.put(Events.DESCRIPTION, from.description)
345-
346322
// color
347323
val color = from.color
348324
if (color != null) {
@@ -404,114 +380,4 @@ class LegacyAndroidEventBuilder2(
404380
return row
405381
}
406382

407-
private fun buildAttendee(attendee: Attendee): ContentValues {
408-
val values = ContentValues()
409-
val organizer = event.organizerEmail ?:
410-
/* no ORGANIZER, use current account owner as ORGANIZER */
411-
calendar.ownerAccount ?: calendar.account.name
412-
413-
val member = attendee.calAddress
414-
if (member.scheme.equals("mailto", true)) // attendee identified by email
415-
values.put(Attendees.ATTENDEE_EMAIL, member.schemeSpecificPart)
416-
else {
417-
// attendee identified by other URI
418-
values.put(Attendees.ATTENDEE_ID_NAMESPACE, member.scheme)
419-
values.put(Attendees.ATTENDEE_IDENTITY, member.schemeSpecificPart)
420-
421-
attendee.getParameter<Email>(Parameter.EMAIL)?.let { email ->
422-
values.put(Attendees.ATTENDEE_EMAIL, email.value)
423-
}
424-
}
425-
426-
attendee.getParameter<Cn>(Parameter.CN)?.let { cn ->
427-
values.put(Attendees.ATTENDEE_NAME, cn.value)
428-
}
429-
430-
// type/relation mapping is complex and thus outsourced to AttendeeMappings
431-
AttendeeMappings.iCalendarToAndroid(attendee, values, organizer)
432-
433-
val status = when(attendee.getParameter(Parameter.PARTSTAT) as? PartStat) {
434-
PartStat.ACCEPTED -> Attendees.ATTENDEE_STATUS_ACCEPTED
435-
PartStat.DECLINED -> Attendees.ATTENDEE_STATUS_DECLINED
436-
PartStat.TENTATIVE -> Attendees.ATTENDEE_STATUS_TENTATIVE
437-
PartStat.DELEGATED -> Attendees.ATTENDEE_STATUS_NONE
438-
else /* default: PartStat.NEEDS_ACTION */ -> Attendees.ATTENDEE_STATUS_INVITED
439-
}
440-
values.put(Attendees.ATTENDEE_STATUS, status)
441-
442-
return values
443-
}
444-
445-
private fun buildReminder(alarm: VAlarm): ContentValues {
446-
val method = when (alarm.action?.value?.uppercase(Locale.ROOT)) {
447-
Action.DISPLAY.value,
448-
Action.AUDIO.value -> Reminders.METHOD_ALERT // will trigger an alarm on the Android device
449-
450-
// Note: The calendar provider doesn't support saving specific attendees for email reminders.
451-
Action.EMAIL.value -> Reminders.METHOD_EMAIL
452-
453-
else -> Reminders.METHOD_DEFAULT // won't trigger an alarm on the Android device
454-
}
455-
456-
val minutes = ICalendar.vAlarmToMin(alarm, event, false)?.second ?: Reminders.MINUTES_DEFAULT
457-
458-
return contentValuesOf(
459-
Reminders.METHOD to method,
460-
Reminders.MINUTES to minutes
461-
)
462-
}
463-
464-
private fun buildCategories(categories: List<String>): ContentValues {
465-
// concatenate, separate by backslash
466-
val rawCategories = categories.joinToString(AndroidEvent2.CATEGORIES_SEPARATOR.toString()) { category ->
467-
// drop occurrences of CATEGORIES_SEPARATOR in category names
468-
category.filter { it != AndroidEvent2.CATEGORIES_SEPARATOR }
469-
}
470-
return contentValuesOf(
471-
ExtendedProperties.NAME to AndroidEvent2.EXTNAME_CATEGORIES,
472-
ExtendedProperties.VALUE to rawCategories
473-
)
474-
}
475-
476-
/**
477-
* Retain classification other than PUBLIC and PRIVATE as unknown property so
478-
* that it can be reused when "server default" is selected.
479-
*
480-
* Should not be returned as an unknown property in the future, but as explicit extended property.
481-
*/
482-
private fun buildRetainedClassification(classification: Clazz): ContentValues? {
483-
if (classification != Clazz.PUBLIC && classification != Clazz.PRIVATE)
484-
return contentValuesOf(
485-
ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE,
486-
ExtendedProperties.VALUE to UnknownProperty.toJsonString(classification)
487-
)
488-
return null
489-
}
490-
491-
private fun buildUnknownProperty(property: Property): ContentValues? {
492-
if (property.value == null) {
493-
logger.warning("Ignoring unknown property with null value")
494-
return null
495-
}
496-
if (property.value.length > UnknownProperty.MAX_UNKNOWN_PROPERTY_SIZE) {
497-
logger.warning("Ignoring unknown property with ${property.value.length} octets (too long)")
498-
return null
499-
}
500-
501-
return contentValuesOf(
502-
ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE,
503-
ExtendedProperties.VALUE to UnknownProperty.toJsonString(property)
504-
)
505-
}
506-
507-
private fun buildUrl(url: String) = contentValuesOf(
508-
ExtendedProperties.NAME to AndroidEvent2.EXTNAME_URL,
509-
ExtendedProperties.VALUE to url
510-
)
511-
512-
513-
private fun fieldBuilders(): Array<AndroidEventFieldBuilder> = arrayOf(
514-
TitleBuilder()
515-
)
516-
517383
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ package at.bitfire.synctools.mapping.calendar.builder
99
import android.content.Entity
1010
import at.bitfire.ical4android.Event
1111

12-
interface AndroidEventFieldBuilder {
12+
interface AndroidEntityBuilder {
1313

1414
/**
15-
* Maps the given event into the provided [Entity].
15+
* Maps a specific part of the given event into the provided [Entity].
1616
*
1717
* If [from] references the same object as [main], this method is called for a main event (not an exception).
1818
* If [from] references another object as [main], this method is called for an exception (not a main event).
@@ -25,7 +25,8 @@ interface AndroidEventFieldBuilder {
2525
*
2626
* Note: The result of the mapping is used to either create or update the event row in the content provider.
2727
* For updates, explicit `null` values are required for fields that should be `null` (otherwise the value
28-
* wouldn't be updated to `null` in case of an event update).
28+
* wouldn't be updated to `null` in case of an event update). Sub-rows of the [Entity] will always be created
29+
* anew, so there's no need to use `null` values in sub-rows.
2930
*
3031
* @param from event to map
3132
* @param main main event
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.builder
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 at.bitfire.synctools.storage.calendar.AndroidCalendar
15+
import net.fortuna.ical4j.model.Parameter
16+
import net.fortuna.ical4j.model.parameter.Cn
17+
import net.fortuna.ical4j.model.parameter.Email
18+
import net.fortuna.ical4j.model.parameter.PartStat
19+
import net.fortuna.ical4j.model.property.Attendee
20+
21+
class AttendeesBuilder(
22+
private val calendar: AndroidCalendar
23+
): AndroidEntityBuilder {
24+
25+
override fun build(from: Event, main: Event, to: Entity) {
26+
for (attendee in from.attendees)
27+
to.addSubValue(Attendees.CONTENT_URI, buildAttendee(attendee, from))
28+
}
29+
30+
private fun buildAttendee(attendee: Attendee, event: Event): ContentValues {
31+
val values = ContentValues()
32+
val organizer = event.organizerEmail ?:
33+
/* no ORGANIZER, use current account owner as ORGANIZER */
34+
calendar.ownerAccount ?: calendar.account.name
35+
36+
val member = attendee.calAddress
37+
if (member.scheme.equals("mailto", true)) // attendee identified by email
38+
values.put(Attendees.ATTENDEE_EMAIL, member.schemeSpecificPart)
39+
else {
40+
// attendee identified by other URI
41+
values.put(Attendees.ATTENDEE_ID_NAMESPACE, member.scheme)
42+
values.put(Attendees.ATTENDEE_IDENTITY, member.schemeSpecificPart)
43+
44+
attendee.getParameter<Email>(Parameter.EMAIL)?.let { email ->
45+
values.put(Attendees.ATTENDEE_EMAIL, email.value)
46+
}
47+
}
48+
49+
attendee.getParameter<Cn>(Parameter.CN)?.let { cn ->
50+
values.put(Attendees.ATTENDEE_NAME, cn.value)
51+
}
52+
53+
// type/relation mapping is complex and thus outsourced to AttendeeMappings
54+
AttendeeMappings.iCalendarToAndroid(attendee, values, organizer)
55+
56+
val status = when(attendee.getParameter(Parameter.PARTSTAT) as? PartStat) {
57+
PartStat.ACCEPTED -> Attendees.ATTENDEE_STATUS_ACCEPTED
58+
PartStat.DECLINED -> Attendees.ATTENDEE_STATUS_DECLINED
59+
PartStat.TENTATIVE -> Attendees.ATTENDEE_STATUS_TENTATIVE
60+
PartStat.DELEGATED -> Attendees.ATTENDEE_STATUS_NONE
61+
else /* default: PartStat.NEEDS_ACTION */ -> Attendees.ATTENDEE_STATUS_INVITED
62+
}
63+
values.put(Attendees.ATTENDEE_STATUS, status)
64+
65+
return values
66+
}
67+
68+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.builder
8+
9+
import android.content.Entity
10+
import android.provider.CalendarContract.ExtendedProperties
11+
import androidx.core.content.contentValuesOf
12+
import at.bitfire.ical4android.Event
13+
import at.bitfire.synctools.storage.calendar.AndroidEvent2
14+
15+
class CategoriesBuilder: AndroidEntityBuilder {
16+
17+
override fun build(from: Event, main: Event, to: Entity) {
18+
val categories = from.categories
19+
if (categories.isNotEmpty()) {
20+
val rawCategories = categories.joinToString(AndroidEvent2.CATEGORIES_SEPARATOR.toString()) { category ->
21+
// drop occurrences of CATEGORIES_SEPARATOR in category names
22+
category.filter { it != AndroidEvent2.CATEGORIES_SEPARATOR }
23+
}
24+
25+
to.addSubValue(
26+
ExtendedProperties.CONTENT_URI,
27+
contentValuesOf(
28+
ExtendedProperties.NAME to AndroidEvent2.EXTNAME_CATEGORIES,
29+
ExtendedProperties.VALUE to rawCategories
30+
)
31+
)
32+
}
33+
}
34+
35+
}

0 commit comments

Comments
 (0)