Skip to content

Commit 8560610

Browse files
committed
Update AndroidEventProcessor to ensure events have UIDs
- Add UidGenerator interface for generating new UIDs
1 parent 839adda commit 8560610

File tree

7 files changed

+79
-51
lines changed

7 files changed

+79
-51
lines changed

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

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

99
import android.content.Entity
1010
import android.provider.CalendarContract.Events
11+
import android.provider.CalendarContract.ExtendedProperties
1112
import at.bitfire.synctools.icalendar.AssociatedEvents
1213
import at.bitfire.synctools.mapping.calendar.processor.AccessLevelProcessor
1314
import at.bitfire.synctools.mapping.calendar.processor.AndroidEventFieldProcessor
@@ -28,10 +29,12 @@ import at.bitfire.synctools.mapping.calendar.processor.SequenceProcessor
2829
import at.bitfire.synctools.mapping.calendar.processor.StartTimeProcessor
2930
import at.bitfire.synctools.mapping.calendar.processor.StatusProcessor
3031
import at.bitfire.synctools.mapping.calendar.processor.TitleProcessor
32+
import at.bitfire.synctools.mapping.calendar.processor.UidGenerator
3133
import at.bitfire.synctools.mapping.calendar.processor.UidProcessor
3234
import at.bitfire.synctools.mapping.calendar.processor.UnknownPropertiesProcessor
3335
import at.bitfire.synctools.mapping.calendar.processor.UrlProcessor
3436
import at.bitfire.synctools.storage.calendar.EventAndExceptions
37+
import at.bitfire.synctools.storage.calendar.EventsContract
3538
import net.fortuna.ical4j.model.DateList
3639
import net.fortuna.ical4j.model.Property
3740
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
@@ -47,11 +50,13 @@ import java.util.LinkedList
4750
* Mapper from Android event main + data rows to [VEvent].
4851
*
4952
* @param accountName account name (used to generate self-attendee)
50-
* @param prodIdGenerator generator for `PRODID`
53+
* @param uidGenerator generates a new `UID`, if necessary
54+
* @param prodIdGenerator generates the `PRODID` to use
5155
*/
5256
class AndroidEventProcessor(
5357
accountName: String,
54-
private val prodIdGenerator: ProdIdGenerator
58+
private val prodIdGenerator: ProdIdGenerator,
59+
private val uidGenerator: UidGenerator
5560
) {
5661

5762
private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()
@@ -84,14 +89,23 @@ class AndroidEventProcessor(
8489
)
8590

8691

87-
fun populate(eventAndExceptions: EventAndExceptions): AssociatedEvents {
88-
// main event
92+
/**
93+
* Maps an Android event with its exceptions to VEVENTs.
94+
*
95+
* VEVENTs must have a valid UID, so this method (or better so say, the [UidProcessor] that it calls)
96+
* generates an UID by calling [uidGenerator], if necessary.
97+
*/
98+
fun toVEvents(eventAndExceptions: EventAndExceptions): AssociatedEvents {
99+
// make sure that main event has a UID
100+
provideUid(eventAndExceptions.main)
101+
102+
// map main event
89103
val main = populateEvent(
90104
entity = eventAndExceptions.main,
91105
main = eventAndExceptions.main
92106
)
93107

94-
// Add exceptions of recurring main event
108+
// add exceptions of recurring main event
95109
val rRules = main.getProperties<RRule>(Property.RRULE)
96110
val rDates = main.getProperties<RDate>(Property.RDATE)
97111
val exceptions = LinkedList<VEvent>()
@@ -160,6 +174,37 @@ class AndroidEventProcessor(
160174
return vEvent
161175
}
162176

177+
/**
178+
* Makes sure that the event has a UID ([Events.UID_2445] in the main event row).
179+
*
180+
* If the event doesn't have a UID, a new one is generated using [uidGenerator] and
181+
* put into [entity].
182+
*
183+
* @param entity event to be checked (**will be modified** if it doesn't already have a UID)
184+
*/
185+
private fun provideUid(entity: Entity) {
186+
val mainValues = entity.entityValues
187+
if (mainValues.getAsString(Events.UID_2445) != null) {
188+
// UID already present, nothing to do
189+
return
190+
}
191+
192+
// have a look at extended properties (Google Calendar)
193+
val googleCalendarUid = entity.subValues.firstOrNull {
194+
it.uri == ExtendedProperties.CONTENT_URI &&
195+
it.values.getAsString(ExtendedProperties.NAME) == EventsContract.EXTNAME_GOOGLE_CALENDAR_UID
196+
}?.values?.getAsString(ExtendedProperties.VALUE)
197+
if (googleCalendarUid != null) {
198+
// copy to UID_2445 so that it will be processed by UidProcessor and return
199+
mainValues.put(Events.UID_2445, googleCalendarUid)
200+
return
201+
}
202+
203+
// still no UID, generate one
204+
val newUid = uidGenerator.createUid()
205+
mainValues.put(Events.UID_2445, newUid)
206+
}
207+
163208

164209
companion object {
165210

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
fun interface UidGenerator {
10+
11+
fun createUid(): String
12+
13+
}

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

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,17 @@ package at.bitfire.synctools.mapping.calendar.processor
88

99
import android.content.Entity
1010
import android.provider.CalendarContract.Events
11-
import android.provider.CalendarContract.ExtendedProperties
12-
import at.bitfire.synctools.storage.calendar.EventsContract
11+
import at.bitfire.synctools.exception.InvalidLocalResourceException
1312
import net.fortuna.ical4j.model.component.VEvent
1413
import net.fortuna.ical4j.model.property.Uid
1514

16-
class UidProcessor: AndroidEventFieldProcessor {
15+
class UidProcessor(): AndroidEventFieldProcessor {
1716

1817
override fun process(from: Entity, main: Entity, to: VEvent) {
19-
// Always take the UID from the main event because exceptions must have the same UID anyway.
20-
val uid = uidFromEntity(main)
21-
if (uid != null)
22-
to.properties += Uid(uid)
23-
}
24-
25-
26-
companion object {
27-
28-
/**
29-
* Gets the UID from:
30-
*
31-
* - main event row (because exceptions must have the same UID anyway) or
32-
* - main event's Google Calendar extended property.
33-
*
34-
* @param entity Android event to extract the UID from
35-
*
36-
* @return UID (or *null* if [entity] doesn't have an UID)
37-
*/
38-
fun uidFromEntity(entity: Entity): String? =
39-
entity.entityValues.getAsString(Events.UID_2445)
40-
?: uidFromExtendedProperties(entity.subValues)
41-
42-
private fun uidFromExtendedProperties(rows: List<Entity.NamedContentValues>): String? {
43-
val uidRow = rows.firstOrNull {
44-
it.uri == ExtendedProperties.CONTENT_URI &&
45-
it.values.getAsString(ExtendedProperties.NAME) == EventsContract.EXTNAME_ICAL_UID
46-
}
47-
return uidRow?.values?.getAsString(ExtendedProperties.VALUE)
48-
}
18+
val uid = main.entityValues.getAsString(Events.UID_2445)
19+
?: throw InvalidLocalResourceException("Main event doesn't have a UID")
4920

21+
to.properties += Uid(uid)
5022
}
5123

5224
}

lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/AndroidCalendar.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ class AndroidCalendar(
386386
arrayOf(
387387
eventId.toString(),
388388
EventsContract.EXTNAME_CATEGORIES,
389-
EventsContract.EXTNAME_ICAL_UID, // UID is stored in UID_2445, don't leave iCalUid rows in events that we have written
389+
EventsContract.EXTNAME_GOOGLE_CALENDAR_UID, // UID is stored in UID_2445, don't leave iCalUid rows in events that we have written
390390
EventsContract.EXTNAME_URL,
391391
UnknownProperty.CONTENT_ITEM_TYPE
392392
)

lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/EventsContract.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,8 @@ object EventsContract {
6262
/**
6363
* Google Calendar uses an extended property called `iCalUid` for storing the event's UID, instead of the
6464
* standard [CalendarContract.EventsColumns.UID_2445].
65-
*
66-
* @see <a href="https://github.com/bitfireAT/ical4android/issues/125">GitHub Issue</a>
6765
*/
68-
const val EXTNAME_ICAL_UID = "iCalUid"
66+
const val EXTNAME_GOOGLE_CALENDAR_UID = "iCalUid"
6967

7068
/**
7169
* VEVENT URL is stored as an extended property with this [CalendarContract.ExtendedPropertiesColumns.NAME].

lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/AndroidEventProcessorTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class AndroidEventProcessorTest {
4747

4848
@Test
4949
fun `Generated event has DTSTAMP`() {
50-
val result = processor.populate(
50+
val result = processor.process(
5151
eventAndExceptions = EventAndExceptions(
5252
main = Entity(contentValuesOf(
5353
Events.DTSTART to 1594056600000L
@@ -61,7 +61,7 @@ class AndroidEventProcessorTest {
6161

6262
@Test
6363
fun `Exception is processed`() {
64-
val result = processor.populate(
64+
val result = processor.process(
6565
eventAndExceptions = EventAndExceptions(
6666
main = Entity(contentValuesOf(
6767
Events.TITLE to "Recurring non-all-day event with exception",
@@ -94,7 +94,7 @@ class AndroidEventProcessorTest {
9494

9595
@Test
9696
fun `Exception is ignored when there's only one invalid RRULE`() {
97-
val result = processor.populate(
97+
val result = processor.process(
9898
eventAndExceptions = EventAndExceptions(
9999
main = Entity(contentValuesOf(
100100
Events.TITLE to "Factically non-recurring non-all-day event with exception",
@@ -124,7 +124,7 @@ class AndroidEventProcessorTest {
124124

125125
@Test
126126
fun `Cancelled exception becomes EXDATE`() {
127-
val result = processor.populate(
127+
val result = processor.process(
128128
eventAndExceptions = EventAndExceptions(
129129
main = Entity(contentValuesOf(
130130
Events.TITLE to "Recurring all-day event with cancelled exception",
@@ -155,7 +155,7 @@ class AndroidEventProcessorTest {
155155

156156
@Test
157157
fun `Cancelled exception without RECURRENCE-ID is ignored`() {
158-
val result = processor.populate(
158+
val result = processor.process(
159159
eventAndExceptions = EventAndExceptions(
160160
main = Entity(contentValuesOf(
161161
Events.TITLE to "Recurring all-day event with cancelled exception and no RECURRENCE-ID",
@@ -186,7 +186,7 @@ class AndroidEventProcessorTest {
186186

187187
@Test
188188
fun `Empty packages for PRODID`() {
189-
val result = processor.populate(
189+
val result = processor.process(
190190
eventAndExceptions = EventAndExceptions(
191191
main = Entity(contentValuesOf(
192192
Events.DTSTART to 1594056600000L
@@ -199,7 +199,7 @@ class AndroidEventProcessorTest {
199199

200200
@Test
201201
fun `Two packages for PRODID`() {
202-
val result = processor.populate(
202+
val result = processor.process(
203203
eventAndExceptions = EventAndExceptions(
204204
main = Entity(contentValuesOf(
205205
Events.DTSTART to 1594056600000L,

lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/UidProcessorTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class UidProcessorTest {
4646
fun `UID from extended row`() {
4747
val entity = Entity(ContentValues())
4848
entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf(
49-
ExtendedProperties.NAME to EventsContract.EXTNAME_ICAL_UID,
49+
ExtendedProperties.NAME to EventsContract.EXTNAME_GOOGLE_CALENDAR_UID,
5050
ExtendedProperties.VALUE to "from-extended"
5151
))
5252
val result = VEvent()
@@ -60,7 +60,7 @@ class UidProcessorTest {
6060
Events.UID_2445 to "from-event"
6161
))
6262
entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf(
63-
ExtendedProperties.NAME to EventsContract.EXTNAME_ICAL_UID,
63+
ExtendedProperties.NAME to EventsContract.EXTNAME_GOOGLE_CALENDAR_UID,
6464
ExtendedProperties.VALUE to "from-extended"
6565
))
6666
val result = VEvent()

0 commit comments

Comments
 (0)