77package at.bitfire.synctools.mapping.calendar
88
99import android.content.Entity
10+ import android.provider.CalendarContract
1011import android.provider.CalendarContract.Events
12+ import android.provider.CalendarContract.ExtendedProperties
13+ import androidx.annotation.VisibleForTesting
1114import at.bitfire.synctools.icalendar.AssociatedEvents
1215import at.bitfire.synctools.mapping.calendar.processor.AccessLevelProcessor
1316import at.bitfire.synctools.mapping.calendar.processor.AndroidEventFieldProcessor
@@ -21,7 +24,6 @@ import at.bitfire.synctools.mapping.calendar.processor.EndTimeProcessor
2124import at.bitfire.synctools.mapping.calendar.processor.LocationProcessor
2225import at.bitfire.synctools.mapping.calendar.processor.OrganizerProcessor
2326import at.bitfire.synctools.mapping.calendar.processor.OriginalInstanceTimeProcessor
24- import at.bitfire.synctools.mapping.calendar.processor.ProdIdGenerator
2527import at.bitfire.synctools.mapping.calendar.processor.RecurrenceFieldsProcessor
2628import at.bitfire.synctools.mapping.calendar.processor.RemindersProcessor
2729import at.bitfire.synctools.mapping.calendar.processor.SequenceProcessor
@@ -32,6 +34,7 @@ import at.bitfire.synctools.mapping.calendar.processor.UidProcessor
3234import at.bitfire.synctools.mapping.calendar.processor.UnknownPropertiesProcessor
3335import at.bitfire.synctools.mapping.calendar.processor.UrlProcessor
3436import at.bitfire.synctools.storage.calendar.EventAndExceptions
37+ import at.bitfire.synctools.storage.calendar.EventsContract
3538import net.fortuna.ical4j.model.DateList
3639import net.fortuna.ical4j.model.Property
3740import net.fortuna.ical4j.model.TimeZoneRegistryFactory
@@ -42,12 +45,13 @@ import net.fortuna.ical4j.model.property.RDate
4245import net.fortuna.ical4j.model.property.RRule
4346import net.fortuna.ical4j.model.property.RecurrenceId
4447import java.util.LinkedList
48+ import java.util.UUID
4549
4650/* *
4751 * Mapper from Android event main + data rows to [VEvent].
4852 *
49- * @param accountName account name (used to generate self-attendee )
50- * @param prodIdGenerator generator for `PRODID`
53+ * @param accountName account name (used to generate reminder email address )
54+ * @param prodIdGenerator generates the `PRODID` to use
5155 */
5256class AndroidEventProcessor (
5357 accountName : String ,
@@ -84,21 +88,55 @@ class AndroidEventProcessor(
8488 )
8589
8690
87- fun populate (eventAndExceptions : EventAndExceptions ): AssociatedEvents {
88- // main event
89- val main = populateEvent(
91+ /* *
92+ * Result of the [mapToVEvents] operation.
93+ *
94+ * @param associatedEvents mapped events
95+ * @param uid UID of the mapped events
96+ * @param generatedUid whether [uid] was generated by [mapToVEvents] (*false*: `UID` was already present before mapping)
97+ * @param updatedSequence the new increased `SEQUENCE` of the main event (*null* if sequence was not increased by [mapToVEvents])
98+ */
99+ class MappingResult (
100+ val associatedEvents : AssociatedEvents ,
101+ val uid : String ,
102+ val generatedUid : Boolean ,
103+ val updatedSequence : Int?
104+ )
105+
106+ /* *
107+ * Maps an Android event with its exceptions to VEVENTs.
108+ *
109+ * VEVENTs must have a valid `UID`, so this method (or better to say, the [UidProcessor] that it calls)
110+ * generates an UID, if necessary. If an `UID` was generated, it is noted in the result.
111+ *
112+ * This method also increases the `SEQUENCE`, if necessary, so that the mapped `SEQUENCE`
113+ * can be different than the original `SEQUENCE` in [eventAndExceptions]. If the `SEQUENCE` was
114+ * increased, it is noted in the result.
115+ */
116+ fun mapToVEvents (eventAndExceptions : EventAndExceptions ): MappingResult {
117+ // make sure that main event has a UID
118+ var generatedUid = false
119+ val uid = provideUid(eventAndExceptions.main) {
120+ generatedUid = true
121+ UUID .randomUUID().toString()
122+ }
123+
124+ val updatedSequence = increaseSequence(eventAndExceptions.main)
125+
126+ // map main event
127+ val main = mapEvent(
90128 entity = eventAndExceptions.main,
91129 main = eventAndExceptions.main
92130 )
93131
94- // Add exceptions of recurring main event
132+ // add exceptions of recurring main event
95133 val rRules = main.getProperties<RRule >(Property .RRULE )
96134 val rDates = main.getProperties<RDate >(Property .RDATE )
97135 val exceptions = LinkedList <VEvent >()
98136 if (rRules.isNotEmpty() || rDates.isNotEmpty()) {
99137 for (exception in eventAndExceptions.exceptions) {
100138 // convert exception to Event
101- val exceptionEvent = populateEvent (
139+ val exceptionEvent = mapEvent (
102140 entity = exception,
103141 main = eventAndExceptions.main
104142 )
@@ -114,11 +152,17 @@ class AndroidEventProcessor(
114152 }
115153 }
116154
117- return AssociatedEvents (
155+ val mappedEvents = AssociatedEvents (
118156 main = main,
119157 exceptions = exceptions,
120158 prodId = generateProdId(eventAndExceptions.main)
121159 )
160+ return MappingResult (
161+ associatedEvents = mappedEvents,
162+ uid = uid,
163+ generatedUid = generatedUid,
164+ updatedSequence = updatedSequence
165+ )
122166 }
123167
124168 private fun asExDate (entity : Entity , recurrenceId : RecurrenceId ): ExDate {
@@ -145,6 +189,61 @@ class AndroidEventProcessor(
145189 return prodIdGenerator.generateProdId(packages)
146190 }
147191
192+ /* *
193+ * Increases the event's SEQUENCE, if necessary.
194+ *
195+ * @param main event to be checked (**will be modified** when SEQUENCE needs to be increased)
196+ *
197+ * @return updated sequence (or *null* if sequence was not increased/modified)
198+ */
199+ @VisibleForTesting
200+ internal fun increaseSequence (main : Entity ): Int? {
201+ val mainValues = main.entityValues
202+ val currentSeq = mainValues.getAsInteger(EventsContract .COLUMN_SEQUENCE )
203+
204+ if (currentSeq == null ) {
205+ /* First upload, request to set to 0 in calendar provider after upload.
206+ We can let it empty in the Entity because then no SEQUENCE property will be generated,
207+ which is equal to SEQUENCE:0. */
208+ return 0
209+ }
210+
211+ val groupScheduled = main.subValues.any { it.uri == CalendarContract .Attendees .CONTENT_URI }
212+ if (groupScheduled) {
213+ /* Note: Events.IS_ORGANIZER is defined in CalendarDatabaseHelper.java as
214+ COALESCE(Events.IS_ORGANIZER, Events.ORGANIZER = Calendars.OWNER_ACCOUNT), so it's non-null when it's
215+ - either explicitly set for an event,
216+ - or the event's ORGANIZER is the same as the calendar's OWNER_ACCOUNT. */
217+ val weAreOrganizer = when (mainValues.getAsInteger(Events .IS_ORGANIZER )) {
218+ null , 0 -> false
219+ /* explicitly set to non-zero, or 1 by provider calculation */ else -> true
220+ }
221+
222+ return if (weAreOrganizer) {
223+ /* Upload of a group-scheduled event and we are the organizer, so we increase the SEQUENCE.
224+ We also have to store it into the Entity so that the new value will be mapped. */
225+ (currentSeq + 1 ).also { newSeq ->
226+ mainValues.put(EventsContract .COLUMN_SEQUENCE , newSeq)
227+ }
228+ } else
229+ /* Upload of a group-scheduled event and we are not the organizer, so we don't increase the SEQUENCE. */
230+ null
231+
232+ } else /* not group-scheduled */ {
233+ return if (currentSeq == 0 ) {
234+ /* The event was uploaded once and has SEQUENCE of 0 (which is mapped to an empty SEQUENCE property).
235+ We don't need to increase the SEQUENCE because the event is not group-scheduled. */
236+ null
237+ } else {
238+ /* Upload of a non-group-scheduled event where a SEQUENCE > 0 is present. Increase by one after upload.
239+ We also have to store it into the Entity so that the new value will be mapped. */
240+ (currentSeq + 1 ).also { newSeq ->
241+ mainValues.put(EventsContract .COLUMN_SEQUENCE , newSeq)
242+ }
243+ }
244+ }
245+ }
246+
148247 /* *
149248 * Reads data of an event from the calendar provider, i.e. converts the [entity] values into a [VEvent].
150249 *
@@ -153,13 +252,51 @@ class AndroidEventProcessor(
153252 *
154253 * @return generated data object
155254 */
156- private fun populateEvent (entity : Entity , main : Entity ): VEvent {
157- val vEvent = VEvent ()
255+ private fun mapEvent (entity : Entity , main : Entity ): VEvent {
256+ // initialization adds DTSTAMP
257+ val vEvent = VEvent (/* initialise = */ true )
258+
158259 for (processor in fieldProcessors)
159260 processor.process(from = entity, main = main, to = vEvent)
160261 return vEvent
161262 }
162263
264+ /* *
265+ * Makes sure that the event has a UID ([Events.UID_2445] in the main event row).
266+ *
267+ * If the event doesn't have a UID, a new one is generated using [generateUid] and
268+ * put into [main].
269+ *
270+ * @param main event to be checked (**will be modified** if it doesn't already have a UID)
271+ */
272+ private fun provideUid (
273+ main : Entity ,
274+ generateUid : () -> String
275+ ): String {
276+ val mainValues = main.entityValues
277+ val existingUid = mainValues.getAsString(Events .UID_2445 )
278+ if (existingUid != null ) {
279+ // UID already present, nothing to do
280+ return existingUid
281+ }
282+
283+ // have a look at extended properties (Google Calendar)
284+ val googleCalendarUid = main.subValues.firstOrNull {
285+ it.uri == ExtendedProperties .CONTENT_URI &&
286+ it.values.getAsString(ExtendedProperties .NAME ) == EventsContract .EXTNAME_GOOGLE_CALENDAR_UID
287+ }?.values?.getAsString(ExtendedProperties .VALUE )
288+ if (googleCalendarUid != null ) {
289+ // copy to UID_2445 so that it will be processed by UidProcessor and return
290+ mainValues.put(Events .UID_2445 , googleCalendarUid)
291+ return googleCalendarUid
292+ }
293+
294+ // still no UID, generate one
295+ val newUid = generateUid()
296+ mainValues.put(Events .UID_2445 , newUid)
297+ return newUid
298+ }
299+
163300
164301 companion object {
165302
0 commit comments