mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2025-12-23 23:17:50 -05:00
Refactor sequence handling in calendar sync (#1789)
* Refactor sequence handling in calendar sync - Move sequence update logic to SequenceUpdater - Update LocalCalendar to use new SequenceUpdater - Remove redundant methods from LocalCalendar - Update tests and dependencies * Minor KDoc
This commit is contained in:
@@ -16,7 +16,6 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.ical4android.util.MiscUtils.closeCompat
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendar
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
|
||||
import at.bitfire.synctools.storage.calendar.EventAndExceptions
|
||||
import at.bitfire.synctools.storage.calendar.EventsContract
|
||||
import at.bitfire.synctools.test.InitCalendarProviderRule
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
@@ -66,86 +65,6 @@ class LocalCalendarTest {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testDeleteDirtyEventsWithoutInstances_NoInstances() {
|
||||
// create recurring event with only deleted/cancelled instances
|
||||
val now = System.currentTimeMillis()
|
||||
val id = calendar.add(EventAndExceptions(
|
||||
main = Entity(contentValuesOf(
|
||||
Events._SYNC_ID to "event-without-instances",
|
||||
Events.CALENDAR_ID to calendar.androidCalendar.id,
|
||||
Events.ALL_DAY to 0,
|
||||
Events.DTSTART to now,
|
||||
Events.RRULE to "FREQ=DAILY;COUNT=3",
|
||||
Events.DIRTY to 1
|
||||
)),
|
||||
exceptions = listOf(
|
||||
Entity(contentValuesOf( // first instance: cancelled
|
||||
Events.CALENDAR_ID to calendar.androidCalendar.id,
|
||||
Events.ORIGINAL_INSTANCE_TIME to now,
|
||||
Events.ORIGINAL_ALL_DAY to 0,
|
||||
Events.DTSTART to now + 86400000,
|
||||
Events.STATUS to Events.STATUS_CANCELED
|
||||
)),
|
||||
Entity(contentValuesOf( // second instance: cancelled
|
||||
Events.CALENDAR_ID to calendar.androidCalendar.id,
|
||||
Events.ORIGINAL_INSTANCE_TIME to now + 86400000,
|
||||
Events.ORIGINAL_ALL_DAY to 0,
|
||||
Events.DTSTART to now + 86400000,
|
||||
Events.STATUS to Events.STATUS_CANCELED
|
||||
)),
|
||||
Entity(contentValuesOf( // third and last instance: cancelled
|
||||
Events.CALENDAR_ID to calendar.androidCalendar.id,
|
||||
Events.ORIGINAL_INSTANCE_TIME to now + 2*86400000,
|
||||
Events.ORIGINAL_ALL_DAY to 0,
|
||||
Events.DTSTART to now + 2*86400000,
|
||||
Events.STATUS to Events.STATUS_CANCELED
|
||||
))
|
||||
)
|
||||
))
|
||||
|
||||
// this method should mark the event as deleted
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is now marked as deleted
|
||||
val result = calendar.androidCalendar.getEventRow(id)!!
|
||||
assertEquals(1, result.getAsInteger(Events.DELETED))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteDirtyEventsWithoutInstances_OneInstanceRemaining() {
|
||||
// create recurring event with only deleted/cancelled instances
|
||||
val now = System.currentTimeMillis()
|
||||
val id = calendar.add(EventAndExceptions(
|
||||
main = Entity(contentValuesOf(
|
||||
Events._SYNC_ID to "event-with-instances",
|
||||
Events.CALENDAR_ID to calendar.androidCalendar.id,
|
||||
Events.ALL_DAY to 0,
|
||||
Events.DTSTART to now,
|
||||
Events.RRULE to "FREQ=DAILY;COUNT=2",
|
||||
Events.DIRTY to 1
|
||||
)),
|
||||
exceptions = listOf(
|
||||
Entity(contentValuesOf( // first instance: cancelled
|
||||
Events.CALENDAR_ID to calendar.androidCalendar.id,
|
||||
Events.ORIGINAL_INSTANCE_TIME to now,
|
||||
Events.ORIGINAL_ALL_DAY to 0,
|
||||
Events.DTSTART to now + 86400000,
|
||||
Events.STATUS to Events.STATUS_CANCELED
|
||||
))
|
||||
// however second instance is NOT cancelled
|
||||
)
|
||||
))
|
||||
|
||||
// this method should mark the event as deleted
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is still marked as dirty, but not as deleted
|
||||
val result = calendar.androidCalendar.getEventRow(id)!!
|
||||
assertEquals(1, result.getAsInteger(Events.DIRTY))
|
||||
assertEquals(0, result.getAsInteger(Events.DELETED))
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that [LocalCalendar.removeNotDirtyMarked] works as expected.
|
||||
* @param contentValues values to set on the event. Required:
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.contentValuesOf
|
||||
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
|
||||
import at.bitfire.synctools.storage.BatchOperation
|
||||
import at.bitfire.synctools.storage.calendar.AndroidCalendar
|
||||
import at.bitfire.synctools.storage.calendar.AndroidRecurringCalendar
|
||||
@@ -136,90 +134,4 @@ class LocalCalendar @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun processDirtyExceptions() {
|
||||
// process deleted exceptions
|
||||
logger.info("Processing deleted exceptions")
|
||||
|
||||
androidCalendar.iterateEventRows(
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, EventsContract.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(androidCalendar.id.toString())
|
||||
) { values ->
|
||||
logger.fine("Found deleted exception, removing and re-scheduling original event (if available)")
|
||||
|
||||
val id = values.getAsLong(Events._ID) // can't be null (by definition)
|
||||
val originalID = values.getAsLong(Events.ORIGINAL_ID) // can't be null (by query)
|
||||
|
||||
val batch = CalendarBatchOperation(androidCalendar.client)
|
||||
|
||||
// enqueue: increase sequence of main event
|
||||
val originalEventValues = androidCalendar.getEventRow(originalID, arrayOf(EventsContract.COLUMN_SEQUENCE))
|
||||
val originalSequence = originalEventValues?.getAsInteger(EventsContract.COLUMN_SEQUENCE) ?: 0
|
||||
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(androidCalendar.account))
|
||||
.withValue(EventsContract.COLUMN_SEQUENCE, originalSequence + 1)
|
||||
.withValue(Events.DIRTY, 1)
|
||||
|
||||
// completely remove deleted exception
|
||||
batch += BatchOperation.CpoBuilder.newDelete(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(androidCalendar.account))
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
// process dirty exceptions
|
||||
logger.info("Processing dirty exceptions")
|
||||
androidCalendar.iterateEventRows(
|
||||
arrayOf(Events._ID, Events.ORIGINAL_ID, EventsContract.COLUMN_SEQUENCE),
|
||||
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL",
|
||||
arrayOf(androidCalendar.id.toString())
|
||||
) { values ->
|
||||
logger.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
|
||||
|
||||
val id = values.getAsLong(Events._ID) // can't be null (by definition)
|
||||
val originalID = values.getAsLong(Events.ORIGINAL_ID) // can't be null (by query)
|
||||
val sequence = values.getAsInteger(EventsContract.COLUMN_SEQUENCE) ?: 0
|
||||
|
||||
val batch = CalendarBatchOperation(androidCalendar.client)
|
||||
|
||||
// enqueue: set original event to DIRTY
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(androidCalendar.eventUri(originalID))
|
||||
.withValue(Events.DIRTY, 1)
|
||||
|
||||
// enqueue: increase exception SEQUENCE and set DIRTY to 0
|
||||
batch += BatchOperation.CpoBuilder
|
||||
.newUpdate(androidCalendar.eventUri(id))
|
||||
.withValue(EventsContract.COLUMN_SEQUENCE, sequence + 1)
|
||||
.withValue(Events.DIRTY, 0)
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks dirty events (which are not already marked as deleted) which got no valid instances as "deleted"
|
||||
*
|
||||
* @return number of affected events
|
||||
*/
|
||||
fun deleteDirtyEventsWithoutInstances() {
|
||||
// Iterate dirty main events without exceptions
|
||||
androidCalendar.iterateEventRows(
|
||||
arrayOf(Events._ID),
|
||||
"${Events.DIRTY} AND NOT ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL",
|
||||
null
|
||||
) { values ->
|
||||
val eventId = values.getAsLong(Events._ID)
|
||||
|
||||
// get number of instances
|
||||
val numEventInstances = androidCalendar.numInstances(eventId)
|
||||
|
||||
// delete event if there are no instances
|
||||
if (numEventInstances == 0) {
|
||||
logger.fine("Marking event #$eventId without instances as deleted")
|
||||
androidCalendar.updateEventRow(eventId, contentValuesOf(Events.DELETED to 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
/**
|
||||
* This is an interface between the Syncer/SyncManager and a collection in the local storage.
|
||||
*
|
||||
* It defines operations that are used during sync for all sync data types.
|
||||
*/
|
||||
interface LocalCollection<out T: LocalResource> {
|
||||
|
||||
/** a tag that uniquely identifies the collection (DAVx5-wide) */
|
||||
|
||||
@@ -11,7 +11,9 @@ import at.bitfire.davdroid.resource.LocalResource.Companion.FLAG_REMOTELY_PRESEN
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Defines operations that are used by SyncManager for all sync data types.
|
||||
* This is an interface between the SyncManager and a resource in the local storage.
|
||||
*
|
||||
* It defines operations that are used by SyncManager for all sync data types.
|
||||
*/
|
||||
interface LocalResource {
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import at.bitfire.synctools.icalendar.ICalendarParser
|
||||
import at.bitfire.synctools.mapping.calendar.AndroidEventBuilder
|
||||
import at.bitfire.synctools.mapping.calendar.AndroidEventProcessor
|
||||
import at.bitfire.synctools.mapping.calendar.DefaultProdIdGenerator
|
||||
import at.bitfire.synctools.mapping.calendar.SequenceUpdater
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
@@ -98,10 +99,12 @@ class CalendarSyncManager @AssistedInject constructor(
|
||||
davCollection = DavCalendar(httpClient.okHttpClient, collection.url)
|
||||
|
||||
// if there are dirty exceptions for events, mark their master events as dirty, too
|
||||
localCollection.processDirtyExceptions()
|
||||
val recurringCalendar = localCollection.recurringCalendar
|
||||
recurringCalendar.processDeletedExceptions()
|
||||
recurringCalendar.processDirtyExceptions()
|
||||
|
||||
// now find dirty events that have no instances and set them to deleted
|
||||
localCollection.deleteDirtyEventsWithoutInstances()
|
||||
localCollection.androidCalendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -186,7 +189,10 @@ class CalendarSyncManager @AssistedInject constructor(
|
||||
val localEvent = resource.androidEvent
|
||||
logger.log(Level.FINE, "Preparing upload of event #${resource.id}", localEvent)
|
||||
|
||||
// map Android event to iCalendar (also generates UID and increases SEQUENCE, if necessary)
|
||||
// increase SEQUENCE of main event and remember value
|
||||
val updatedSequence = SequenceUpdater().increaseSequence(localEvent.main)
|
||||
|
||||
// map Android event to iCalendar (also generates UID, if necessary)
|
||||
val processor = AndroidEventProcessor(
|
||||
accountName = resource.recurringCalendar.calendar.account.name,
|
||||
prodIdGenerator = DefaultProdIdGenerator(Constants.iCalProdId)
|
||||
@@ -206,7 +212,7 @@ class CalendarSyncManager @AssistedInject constructor(
|
||||
suggestedFileName = DavUtils.fileNameFromUid(mappedEvents.uid, "ics"),
|
||||
requestBody = requestBody,
|
||||
onSuccessContext = GeneratedResource.OnSuccessContext(
|
||||
sequence = mappedEvents.updatedSequence
|
||||
sequence = updatedSequence
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user