diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt index f0c786329..63465cb55 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalCalendarTest.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.resource import android.accounts.Account import android.content.ContentProviderClient -import android.content.ContentUris import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract @@ -14,22 +13,16 @@ import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf import androidx.test.platform.app.InstrumentationRegistry -import at.bitfire.ical4android.Event -import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter 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.AndroidEvent2 +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 import dagger.hilt.android.testing.HiltAndroidTest -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.RecurrenceId -import net.fortuna.ical4j.model.property.Status import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Rule @@ -74,90 +67,83 @@ class LocalCalendarTest { @Test - fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() { + fun testDeleteDirtyEventsWithoutInstances_NoInstances() { // create recurring event with only deleted/cancelled instances - val event = Event().apply { - dtStart = DtStart("20220120T010203Z") - summary = "Event with 3 instances" - rRules.add(RRule("FREQ=DAILY;COUNT=3")) - exceptions.add(Event().apply { - recurrenceId = RecurrenceId("20220120T010203Z") - dtStart = DtStart("20220120T010203Z") - summary = "Cancelled exception on 1st day" - status = Status.VEVENT_CANCELLED - }) - exceptions.add(Event().apply { - recurrenceId = RecurrenceId("20220121T010203Z") - dtStart = DtStart("20220121T010203Z") - summary = "Cancelled exception on 2nd day" - status = Status.VEVENT_CANCELLED - }) - exceptions.add(Event().apply { - recurrenceId = RecurrenceId("20220122T010203Z") - dtStart = DtStart("20220122T010203Z") - summary = "Cancelled exception on 3rd day" - status = Status.VEVENT_CANCELLED - }) - } - calendar.add( - event = event, - fileName = "filename.ics", - eTag = null, - scheduleTag = null, - flags = LocalResource.FLAG_REMOTELY_PRESENT - ) - val localEvent = calendar.findByName("filename.ics")!! - val eventId = localEvent.id - - // set event as dirty - client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply { - put(Events.DIRTY, 1) - }, null, null) + 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 - client.query( - ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), - arrayOf(Events.DELETED), null, null, null - )!!.use { cursor -> - cursor.moveToNext() - assertEquals(1, cursor.getInt(0)) - } + val result = calendar.androidCalendar.getEventRow(id)!! + assertEquals(1, result.getAsInteger(Events.DELETED)) } @Test - // Needs InitCalendarProviderRule - fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() { - val event = Event().apply { - dtStart = DtStart("20220120T010203Z") - summary = "Event with 3 instances" - rRules.add(RRule("FREQ=DAILY;COUNT=3")) - } - calendar.add( - event = event, - fileName = "filename.ics", - eTag = null, - scheduleTag = null, - flags = LocalResource.FLAG_REMOTELY_PRESENT - ) - val localEvent = calendar.findByName("filename.ics")!! - val eventUrl = androidCalendar.eventUri(localEvent.id) - - // set event as dirty - client.update(eventUrl, contentValuesOf( - Events.DIRTY to 1 - ), null, null) + 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 not marked as deleted - client.query(eventUrl, arrayOf(Events.DELETED), null, null, null)!!.use { cursor -> - cursor.moveToNext() - assertEquals(0, cursor.getInt(0)) - } + // 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)) } /** @@ -167,15 +153,16 @@ class LocalCalendarTest { * - [Events.DIRTY] */ private fun testRemoveNotDirtyMarked(contentValues: ContentValues) { - val id = androidCalendar.addEvent(Entity( + val entity = Entity( contentValuesOf( Events.CALENDAR_ID to androidCalendar.id, Events.DTSTART to System.currentTimeMillis(), Events.DTEND to System.currentTimeMillis(), Events.TITLE to "Some Event", - AndroidEvent2.COLUMN_FLAGS to 123 + EventsContract.COLUMN_FLAGS to 123 ).apply { putAll(contentValues) } - )) + ) + val id = androidCalendar.addEvent(entity) calendar.removeNotDirtyMarked(123) @@ -210,13 +197,13 @@ class LocalCalendarTest { Events.DTSTART to System.currentTimeMillis(), Events.DTEND to System.currentTimeMillis(), Events.TITLE to "Some Event", - AndroidEvent2.COLUMN_FLAGS to 123 + EventsContract.COLUMN_FLAGS to 123 ).apply { putAll(contentValues) } )) val updated = calendar.markNotDirty(321) assertEquals(1, updated) - assertEquals(321, androidCalendar.getEvent(id)?.flags) + assertEquals(321, androidCalendar.getEvent(id)?.entityValues?.getAsInteger(EventsContract.COLUMN_FLAGS)) } @Test diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalEventTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalEventTest.kt deleted file mode 100644 index 5df0bb36a..000000000 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/resource/LocalEventTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - */ - -package at.bitfire.davdroid.resource - -import android.Manifest -import android.accounts.Account -import android.content.ContentProviderClient -import android.content.ContentUris -import android.content.ContentValues -import android.provider.CalendarContract -import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL -import android.provider.CalendarContract.Events -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.GrantPermissionRule -import at.bitfire.ical4android.Event -import at.bitfire.ical4android.util.MiscUtils.closeCompat -import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider -import at.techbee.jtx.JtxContract.asSyncAdapter -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import net.fortuna.ical4j.model.property.DtStart -import net.fortuna.ical4j.model.property.RRule -import net.fortuna.ical4j.model.property.RecurrenceId -import net.fortuna.ical4j.model.property.Status -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import javax.inject.Inject - -@HiltAndroidTest -class LocalEventTest { - - @get:Rule - val hiltRule = HiltAndroidRule(this) - - @get:Rule - val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR) - - @Inject - lateinit var localCalendarFactory: LocalCalendar.Factory - - private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL) - private lateinit var client: ContentProviderClient - private lateinit var calendar: LocalCalendar - - @Before - fun setUp() { - hiltRule.inject() - - val context = InstrumentationRegistry.getInstrumentation().targetContext - client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! - - val provider = AndroidCalendarProvider(account, client) - calendar = localCalendarFactory.create(provider.createAndGetCalendar(ContentValues())) - } - - @After - fun tearDown() { - calendar.androidCalendar.delete() - client.closeCompat() - } - - - @Test - fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() { - // create recurring event with only deleted/cancelled instances - val event = Event().apply { - dtStart = DtStart("20220120T010203Z") - summary = "Event with 3 instances" - rRules.add(RRule("FREQ=DAILY;COUNT=3")) - exceptions.add(Event().apply { - recurrenceId = RecurrenceId("20220120T010203Z") - dtStart = DtStart("20220120T010203Z") - summary = "Cancelled exception on 1st day" - status = Status.VEVENT_CANCELLED - }) - exceptions.add(Event().apply { - recurrenceId = RecurrenceId("20220121T010203Z") - dtStart = DtStart("20220121T010203Z") - summary = "Cancelled exception on 2nd day" - status = Status.VEVENT_CANCELLED - }) - exceptions.add(Event().apply { - recurrenceId = RecurrenceId("20220122T010203Z") - dtStart = DtStart("20220122T010203Z") - summary = "Cancelled exception on 3rd day" - status = Status.VEVENT_CANCELLED - }) - } - calendar.add( - event = event, - fileName = "filename.ics", - eTag = null, - scheduleTag = null, - flags = LocalResource.FLAG_REMOTELY_PRESENT - ) - val localEvent = calendar.findByName("filename.ics")!! - val eventId = localEvent.id!! - - // set event as dirty - client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply { - put(Events.DIRTY, 1) - }, null, null) - - // this method should mark the event as deleted - calendar.deleteDirtyEventsWithoutInstances() - - // verify that event is now marked as deleted - client.query( - ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), - arrayOf(Events.DELETED), null, null, null - )!!.use { cursor -> - cursor.moveToNext() - assertEquals(1, cursor.getInt(0)) - } - } - - @Test - fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() { - val event = Event().apply { - dtStart = DtStart("20220120T010203Z") - summary = "Event with 3 instances" - rRules.add(RRule("FREQ=DAILY;COUNT=3")) - } - calendar.add( - event = event, - fileName = "filename.ics", - eTag = null, - scheduleTag = null, - flags = LocalResource.FLAG_REMOTELY_PRESENT - ) - val localEvent = calendar.findByName("filename.ics")!! - val eventId = localEvent.id!! - - // set event as dirty - client.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply { - put(Events.DIRTY, 1) - }, null, null) - - // this method should mark the event as deleted - calendar.deleteDirtyEventsWithoutInstances() - - // verify that event is not marked as deleted - client.query( - ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), - arrayOf(Events.DELETED), null, null, null - )!!.use { cursor -> - cursor.moveToNext() - assertEquals(0, cursor.getInt(0)) - } - } - -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/CalendarSyncManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/CalendarSyncManagerTest.kt index 024161d87..b9c9ba815 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/CalendarSyncManagerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/CalendarSyncManagerTest.kt @@ -4,16 +4,27 @@ package at.bitfire.davdroid.sync +import android.Manifest import android.accounts.Account +import android.content.ContentProviderClient import android.content.Context +import android.content.Entity +import android.provider.CalendarContract +import android.provider.CalendarContract.Calendars +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import androidx.test.rule.GrantPermissionRule +import at.bitfire.davdroid.resource.LocalCalendar +import at.bitfire.davdroid.resource.LocalEvent import at.bitfire.davdroid.sync.account.TestAccount -import at.bitfire.ical4android.Event +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 dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.every import io.mockk.mockk -import net.fortuna.ical4j.model.property.DtStart import okio.Buffer import org.junit.After import org.junit.Assert.assertEquals @@ -29,34 +40,64 @@ class CalendarSyncManagerTest { @get:Rule val hiltRule = HiltAndroidRule(this) + @get:Rule + val permissionsRule = GrantPermissionRule.grant( + Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR + ) + @Inject @ApplicationContext lateinit var context: Context + @Inject + lateinit var localCalendarFactory: LocalCalendar.Factory + @Inject lateinit var syncManagerFactory: CalendarSyncManager.Factory lateinit var account: Account + lateinit var providerClient: ContentProviderClient + lateinit var androidCalendar: AndroidCalendar + lateinit var localCalendar: LocalCalendar @Before fun setUp() { hiltRule.inject() + account = TestAccount.create() + providerClient = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! + + // create LocalCalendar + val androidCalendarProvider = AndroidCalendarProvider(account, providerClient) + androidCalendar = androidCalendarProvider.createAndGetCalendar(contentValuesOf( + Calendars.NAME to "Sample Calendar" + )) + localCalendar = localCalendarFactory.create(androidCalendar) } @After fun tearDown() { + localCalendar.androidCalendar.delete() + providerClient.closeCompat() TestAccount.remove(account) } @Test - fun generateUpload_existingUid() { - val result = syncManager().generateUpload(mockk(relaxed = true) { - every { getCachedEvent() } returns Event(uid = "existing-uid", dtStart = DtStart()) - }) + fun test_generateUpload_existingUid() { + val result = syncManager().generateUpload(LocalEvent( + localCalendar.recurringCalendar, + EventAndExceptions( + main = Entity(contentValuesOf( + Events._ID to 1, + Events.CALENDAR_ID to androidCalendar.id, + Events.DTSTART to System.currentTimeMillis(), + Events.UID_2445 to "existing-uid" + )), + exceptions = emptyList() + ) + )) assertEquals("existing-uid.ics", result.suggestedFileName) - assertTrue(result.onSuccessContext.uid.isEmpty) val iCal = Buffer().also { result.requestBody.writeTo(it) @@ -66,19 +107,26 @@ class CalendarSyncManagerTest { @Test fun generateUpload_noUid() { - val result = syncManager().generateUpload(mockk(relaxed = true) { - every { getCachedEvent() } returns Event(dtStart = DtStart()) - }) + val result = syncManager().generateUpload(LocalEvent( + localCalendar.recurringCalendar, + EventAndExceptions( + main = Entity(contentValuesOf( + Events._ID to 2, + Events.CALENDAR_ID to androidCalendar.id, + Events.DTSTART to System.currentTimeMillis() + )), + exceptions = emptyList() + ) + )) assertTrue(result.suggestedFileName.matches(UUID_FILENAME_REGEX)) val uuid = result.suggestedFileName.removeSuffix(".ics") - assertEquals(uuid, result.onSuccessContext.uid.get()) - val iCal = Buffer().also { result.requestBody.writeTo(it) }.readString(Charsets.UTF_8) assertTrue(iCal.contains("UID:$uuid\r\n")) + } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt index 9e302cdc6..3f378a317 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt @@ -5,7 +5,6 @@ package at.bitfire.davdroid import at.bitfire.synctools.icalendar.ical4jVersion import ezvcard.Ezvcard -import net.fortuna.ical4j.model.property.ProdId /** * Brand-specific constants like (non-theme) colors, homepage URLs etc. @@ -17,7 +16,7 @@ object Constants { // product IDs for iCalendar/vCard - val iCalProdId = ProdId("DAVx5/${BuildConfig.VERSION_NAME} ical4j/$ical4jVersion") + val iCalProdId = "DAVx5/${BuildConfig.VERSION_NAME} ical4j/$ical4jVersion" const val vCardProdId = "+//IDN bitfire.at//DAVx5/${BuildConfig.VERSION_NAME} ez-vcard/${Ezvcard.VERSION}" } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt index ddb4a70a3..2a1247d39 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt @@ -43,6 +43,7 @@ import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.PropertyList import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.component.VTimeZone +import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.Version import okhttp3.HttpUrl import java.io.StringWriter @@ -376,7 +377,7 @@ class DavCollectionRepository @Inject constructor( Calendar( PropertyList().apply { add(Version.VERSION_2_0) - add(Constants.iCalProdId) + add(ProdId(Constants.iCalProdId)) }, ComponentList( listOf(vTimezone) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt index 2f0318bab..e54186f8c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt @@ -7,15 +7,15 @@ 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.Event import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter -import at.bitfire.synctools.mapping.calendar.LegacyAndroidEventBuilder2 import at.bitfire.synctools.storage.BatchOperation import at.bitfire.synctools.storage.calendar.AndroidCalendar -import at.bitfire.synctools.storage.calendar.AndroidEvent2 import at.bitfire.synctools.storage.calendar.AndroidRecurringCalendar import at.bitfire.synctools.storage.calendar.CalendarBatchOperation +import at.bitfire.synctools.storage.calendar.EventAndExceptions +import at.bitfire.synctools.storage.calendar.EventsContract import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -60,52 +60,42 @@ class LocalCalendar @AssistedInject constructor( androidCalendar.writeSyncState(state.toString()) } - private val recurringCalendar = AndroidRecurringCalendar(androidCalendar) + @VisibleForTesting + internal val recurringCalendar = AndroidRecurringCalendar(androidCalendar) - fun add(event: Event, fileName: String, eTag: String?, scheduleTag: String?, flags: Int) { - val mapped = LegacyAndroidEventBuilder2( - calendar = androidCalendar, - event = event, - syncId = fileName, - eTag = eTag, - scheduleTag = scheduleTag, - flags = flags - ).build() - recurringCalendar.addEventAndExceptions(mapped) + fun add(event: EventAndExceptions): Long { + return recurringCalendar.addEventAndExceptions(event) } override fun findDeleted(): List { val result = LinkedList() - androidCalendar.iterateEvents( "${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null) { entity -> - result += LocalEvent(recurringCalendar, AndroidEvent2(androidCalendar, entity)) + recurringCalendar.iterateEventAndExceptions( + "${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null + ) { eventAndExceptions -> + result += LocalEvent(recurringCalendar, eventAndExceptions) } return result } override fun findDirty(): List { val dirty = LinkedList() - - /* - * RFC 5545 3.8.7.4. Sequence Number - * When a calendar component is created, its sequence number is 0. It is monotonically incremented by the "Organizer's" - * CUA each time the "Organizer" makes a significant revision to the calendar component. - */ - androidCalendar.iterateEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null) { values -> - dirty += LocalEvent(recurringCalendar, AndroidEvent2(androidCalendar, values)) + recurringCalendar.iterateEventAndExceptions( + "${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null + ) { eventAndExceptions -> + dirty += LocalEvent(recurringCalendar, eventAndExceptions) } - return dirty } override fun findByName(name: String) = - androidCalendar.findEvent("${Events._SYNC_ID}=? AND ${Events.ORIGINAL_SYNC_ID} IS null", arrayOf(name))?.let { + recurringCalendar.findEventAndExceptions("${Events._SYNC_ID}=? AND ${Events.ORIGINAL_SYNC_ID} IS null", arrayOf(name))?.let { LocalEvent(recurringCalendar, it) } override fun markNotDirty(flags: Int) = androidCalendar.updateEventRows( - contentValuesOf(AndroidEvent2.COLUMN_FLAGS to flags), + contentValuesOf(EventsContract.COLUMN_FLAGS to flags), // `dirty` can be 0, 1, or null. "NOT dirty" is not enough. """ ${Events.CALENDAR_ID}=? @@ -125,7 +115,7 @@ class LocalCalendar @AssistedInject constructor( ${Events.CALENDAR_ID}=? AND (${Events.DIRTY} IS NULL OR ${Events.DIRTY}=0) AND ${Events.ORIGINAL_ID} IS NULL - AND ${AndroidEvent2.COLUMN_FLAGS}=? + AND ${EventsContract.COLUMN_FLAGS}=? """.trimIndent(), arrayOf(androidCalendar.id.toString(), flags.toString()) ) { values -> @@ -141,7 +131,7 @@ class LocalCalendar @AssistedInject constructor( override fun forgetETags() { androidCalendar.updateEventRows( - contentValuesOf(AndroidEvent2.COLUMN_ETAG to null), + contentValuesOf(EventsContract.COLUMN_ETAG to null), "${Events.CALENDAR_ID}=?", arrayOf(androidCalendar.id.toString()) ) } @@ -152,7 +142,7 @@ class LocalCalendar @AssistedInject constructor( logger.info("Processing deleted exceptions") androidCalendar.iterateEventRows( - arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent2.COLUMN_SEQUENCE), + 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 -> @@ -164,12 +154,12 @@ class LocalCalendar @AssistedInject constructor( val batch = CalendarBatchOperation(androidCalendar.client) // enqueue: increase sequence of main event - val originalEventValues = androidCalendar.getEventRow(originalID, arrayOf(AndroidEvent2.COLUMN_SEQUENCE)) - val originalSequence = originalEventValues?.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE) ?: 0 + 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(AndroidEvent2.COLUMN_SEQUENCE, originalSequence + 1) + .withValue(EventsContract.COLUMN_SEQUENCE, originalSequence + 1) .withValue(Events.DIRTY, 1) // completely remove deleted exception @@ -180,7 +170,7 @@ class LocalCalendar @AssistedInject constructor( // process dirty exceptions logger.info("Processing dirty exceptions") androidCalendar.iterateEventRows( - arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent2.COLUMN_SEQUENCE), + 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 -> @@ -188,7 +178,7 @@ class LocalCalendar @AssistedInject constructor( 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(AndroidEvent2.COLUMN_SEQUENCE) ?: 0 + val sequence = values.getAsInteger(EventsContract.COLUMN_SEQUENCE) ?: 0 val batch = CalendarBatchOperation(androidCalendar.client) @@ -200,7 +190,7 @@ class LocalCalendar @AssistedInject constructor( // enqueue: increase exception SEQUENCE and set DIRTY to 0 batch += BatchOperation.CpoBuilder .newUpdate(androidCalendar.eventUri(id)) - .withValue(AndroidEvent2.COLUMN_SEQUENCE, sequence + 1) + .withValue(EventsContract.COLUMN_SEQUENCE, sequence + 1) .withValue(Events.DIRTY, 0) batch.commit() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt index a92d0b42a..6aa28a574 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalContact.kt @@ -27,7 +27,6 @@ import at.bitfire.vcard4android.AndroidContact import at.bitfire.vcard4android.AndroidContactFactory import at.bitfire.vcard4android.CachedGroupMembership import at.bitfire.vcard4android.Contact -import com.google.common.base.Ascii import com.google.common.base.MoreObjects import java.io.FileNotFoundException import java.util.Optional @@ -139,13 +138,15 @@ class LocalContact: AndroidContact, LocalAddress { .add("fileName", fileName) .add("eTag", eTag) .add("flags", flags) - .add("contact", + /*.add("contact", try { + // too dangerous, may contain unknown properties and cause another OOM Ascii.truncate(getContact().toString(), 1000, "…") } catch (e: Exception) { e } - ).toString() + )*/ + .toString() override fun getViewUri(context: Context): Uri? = id?.let { idNotNull -> diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt index 716e4f513..55bdf9dd3 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt @@ -9,98 +9,70 @@ import android.content.Context import android.provider.CalendarContract import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf -import at.bitfire.ical4android.Event -import at.bitfire.ical4android.LegacyAndroidCalendar -import at.bitfire.synctools.mapping.calendar.LegacyAndroidEventBuilder2 -import at.bitfire.synctools.storage.LocalStorageException -import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.bitfire.synctools.storage.calendar.AndroidCalendar import at.bitfire.synctools.storage.calendar.AndroidRecurringCalendar -import com.google.common.base.Ascii +import at.bitfire.synctools.storage.calendar.EventAndExceptions +import at.bitfire.synctools.storage.calendar.EventsContract import com.google.common.base.MoreObjects import java.util.Optional class LocalEvent( val recurringCalendar: AndroidRecurringCalendar, - val androidEvent: AndroidEvent2 + val androidEvent: EventAndExceptions ) : LocalResource { + val calendar: AndroidCalendar + get() = recurringCalendar.calendar + + private val mainValues = androidEvent.main.entityValues + override val id: Long - get() = androidEvent.id + get() = mainValues.getAsLong(Events._ID) override val fileName: String? - get() = androidEvent.syncId + get() = mainValues.getAsString(Events._SYNC_ID) override val eTag: String? - get() = androidEvent.eTag + get() = mainValues.getAsString(EventsContract.COLUMN_ETAG) override val scheduleTag: String? - get() = androidEvent.scheduleTag + get() = mainValues.getAsString(EventsContract.COLUMN_SCHEDULE_TAG) override val flags: Int - get() = androidEvent.flags + get() = mainValues.getAsInteger(EventsContract.COLUMN_FLAGS) ?: 0 - fun update(data: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) { - val eventAndExceptions = LegacyAndroidEventBuilder2( - calendar = androidEvent.calendar, - event = data, - syncId = fileName, - eTag = eTag, - scheduleTag = scheduleTag, - flags = flags - ).build() - recurringCalendar.updateEventAndExceptions(id, eventAndExceptions) - } - - - private var _event: Event? = null - /** - * Retrieves the event from the content provider and converts it to a legacy data object. - * - * Caches the result: the content provider is only queried at the first call and then - * this method always returns the same object. - * - * @throws LocalStorageException if there is no local event with the ID from [androidEvent] - */ - @Synchronized - fun getCachedEvent(): Event { - _event?.let { return it } - - val legacyCalendar = LegacyAndroidCalendar(androidEvent.calendar) - val event = legacyCalendar.getEvent(androidEvent.id) - ?: throw LocalStorageException("Event ${androidEvent.id} not found") - - _event = event - return event - } - - override fun updateSequence(sequence: Int) { - androidEvent.update(contentValuesOf( - AndroidEvent2.COLUMN_SEQUENCE to sequence - )) - } - - override fun updateUid(uid: String) { - androidEvent.update(contentValuesOf( - Events.UID_2445 to uid - )) + fun update(data: EventAndExceptions) { + recurringCalendar.updateEventAndExceptions(id, data) } override fun clearDirty(fileName: Optional, eTag: String?, scheduleTag: String?) { val values = contentValuesOf( Events.DIRTY to 0, - AndroidEvent2.COLUMN_ETAG to eTag, - AndroidEvent2.COLUMN_SCHEDULE_TAG to scheduleTag + EventsContract.COLUMN_ETAG to eTag, + EventsContract.COLUMN_SCHEDULE_TAG to scheduleTag ) if (fileName.isPresent) values.put(Events._SYNC_ID, fileName.get()) - androidEvent.update(values) + calendar.updateEventRow(id, values) } override fun updateFlags(flags: Int) { - androidEvent.update(contentValuesOf( - AndroidEvent2.COLUMN_FLAGS to flags + calendar.updateEventRow(id, contentValuesOf( + EventsContract.COLUMN_FLAGS to flags + )) + } + + override fun updateSequence(sequence: Int) { + calendar.updateEventRow(id, contentValuesOf( + EventsContract.COLUMN_SEQUENCE to sequence + )) + } + + override fun updateUid(uid: String) { + calendar.updateEventRow(id, contentValuesOf( + Events.UID_2445 to uid )) } @@ -109,7 +81,7 @@ class LocalEvent( } override fun resetDeleted() { - androidEvent.update(contentValuesOf( + calendar.updateEventRow(id, contentValuesOf( Events.DELETED to 0 )) } @@ -123,7 +95,8 @@ class LocalEvent( .add("flags", flags) .add("event", try { - Ascii.truncate(getCachedEvent().toString(), 1000, "…") + // only include truncated main event row (won't contain attachments, unknown properties etc.) + androidEvent.main.entityValues.toString().take(1000) } catch (e: Exception) { e } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt index ba74f4c6a..2148c19c4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt @@ -15,7 +15,6 @@ import at.bitfire.ical4android.DmfsTaskList import at.bitfire.ical4android.Task import at.bitfire.ical4android.TaskProvider import at.bitfire.synctools.storage.BatchOperation -import com.google.common.base.Ascii import com.google.common.base.MoreObjects import org.dmfs.tasks.contract.TaskContract.Tasks import java.util.Optional @@ -122,13 +121,15 @@ class LocalTask: DmfsTask, LocalResource { .add("eTag", eTag) .add("scheduleTag", scheduleTag) .add("flags", flags) - .add("task", + /*.add("task", try { + // too dangerous, may contain unknown properties and cause another OOM Ascii.truncate(task.toString(), 1000, "…") } catch (e: Exception) { e } - ).toString() + )*/ + .toString() override fun getViewUri(context: Context): Uri? { val idNotNull = id ?: return null diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt index 684b1c42f..1f6eddadf 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration12.kt @@ -13,7 +13,7 @@ import android.util.Base64 import androidx.core.content.ContextCompat import androidx.core.content.contentValuesOf import at.bitfire.ical4android.UnknownProperty -import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.bitfire.synctools.storage.calendar.EventsContract import at.techbee.jtx.JtxContract.asSyncAdapter import dagger.Binds import dagger.Module @@ -69,7 +69,7 @@ class AccountSettingsMigration12 @Inject constructor( val property = UnknownProperty.fromJsonString(rawValue) if (property is Url) { // rewrite to MIMETYPE_URL val newValues = contentValuesOf( - CalendarContract.ExtendedProperties.NAME to AndroidEvent2.EXTNAME_URL, + CalendarContract.ExtendedProperties.NAME to EventsContract.EXTNAME_URL, CalendarContract.ExtendedProperties.VALUE to property.value ) provider.update(uri, newValues, null, null) @@ -77,7 +77,7 @@ class AccountSettingsMigration12 @Inject constructor( } catch (e: Exception) { logger.log( Level.WARNING, - "Couldn't rewrite URL from unknown property to ${AndroidEvent2.EXTNAME_URL}", + "Couldn't rewrite URL from unknown property to ${EventsContract.EXTNAME_URL}", e ) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt index 7bcb94a6e..441876232 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt @@ -29,11 +29,14 @@ import at.bitfire.davdroid.resource.SyncState import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.DavUtils import at.bitfire.davdroid.util.DavUtils.lastSegment -import at.bitfire.ical4android.Event -import at.bitfire.ical4android.EventReader -import at.bitfire.ical4android.EventWriter import at.bitfire.ical4android.util.DateUtils import at.bitfire.synctools.exception.InvalidICalendarException +import at.bitfire.synctools.icalendar.CalendarUidSplitter +import at.bitfire.synctools.icalendar.ICalendarGenerator +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 dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -41,6 +44,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.runInterruptible import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.component.VEvent import net.fortuna.ical4j.model.property.Action import okhttp3.HttpUrl import okhttp3.RequestBody.Companion.toRequestBody @@ -179,47 +183,30 @@ class CalendarSyncManager @AssistedInject constructor( } override fun generateUpload(resource: LocalEvent): GeneratedResource { - val event = resource.getCachedEvent() - logger.log(Level.FINE, "Preparing upload of iCalendar ${resource.fileName}", event) + val localEvent = resource.androidEvent + logger.log(Level.FINE, "Preparing upload of event #${resource.id}", localEvent) - // get/create UID - val (uid, uidIsGenerated) = DavUtils.generateUidIfNecessary(event.uid) - if (uidIsGenerated) - event.uid = uid + // map Android event to iCalendar (also generates UID and increases SEQUENCE, if necessary) + val processor = AndroidEventProcessor( + accountName = resource.recurringCalendar.calendar.account.name, + prodIdGenerator = DefaultProdIdGenerator(Constants.iCalProdId) + ) + val mappedEvents = processor.mapToVEvents(localEvent) - // Increase sequence, if necessary: - // - If it's null, the event has just been created in the database, so we can start with SEQUENCE:0 (default). - // - If it's non-null, the event already exists on the server, so increase by one. - val groupScheduled = event.attendees.isNotEmpty() - val weAreOrganizer = event.isOrganizer == true - val sequence = event.sequence - val newSequence: Optional = when { - // first upload, set to 0 after upload - sequence == null -> - Optional.of(0) - - // re-upload of group-scheduled event (and we're ORGANIZER), increase sequence in iCalendar and after upload - groupScheduled && weAreOrganizer -> { - event.sequence = sequence + 1 - Optional.of(sequence + 1) - } - - // standard re-upload, don't update sequence - else -> - Optional.empty() - } + // persist UID if it was generated + if (mappedEvents.generatedUid) + resource.updateUid(mappedEvents.uid) // generate iCalendar and convert to request body val iCalWriter = StringWriter() - EventWriter(Constants.iCalProdId).write(event, iCalWriter) + ICalendarGenerator().write(mappedEvents.associatedEvents, iCalWriter) val requestBody = iCalWriter.toString().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8) return GeneratedResource( - suggestedFileName = DavUtils.fileNameFromUid(uid, "ics"), + suggestedFileName = DavUtils.fileNameFromUid(mappedEvents.uid, "ics"), requestBody = requestBody, onSuccessContext = GeneratedResource.OnSuccessContext( - uid = if (uidIsGenerated) Optional.of(uid) else Optional.empty(), - sequence = newSequence + sequence = mappedEvents.updatedSequence ) ) } @@ -272,11 +259,11 @@ class CalendarSyncManager @AssistedInject constructor( ?: throw DavException("Received multi-get response without ETag") val scheduleTag = response[ScheduleTag::class.java]?.scheduleTag - processVEvent( - response.href.lastSegment, - eTag, - scheduleTag, - StringReader(iCal) + processICalendar( + fileName = response.href.lastSegment, + eTag = eTag, + scheduleTag = scheduleTag, + reader = StringReader(iCal) ) } } @@ -289,56 +276,56 @@ class CalendarSyncManager @AssistedInject constructor( // helpers - private fun processVEvent(fileName: String, eTag: String, scheduleTag: String?, reader: Reader) { - val events: List - try { - events = EventReader().readEvents(reader) - } catch (e: InvalidICalendarException) { - logger.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e) - notifyInvalidResource(e, fileName) + private fun processICalendar(fileName: String, eTag: String, scheduleTag: String?, reader: Reader) { + val calendar = + try { + ICalendarParser().parse(reader) + } catch (e: InvalidICalendarException) { + logger.log(Level.WARNING, "Received invalid iCalendar, ignoring", e) + notifyInvalidResource(e, fileName) + return + } + + val uidsAndEvents = CalendarUidSplitter().associateByUid(calendar, Component.VEVENT) + if (uidsAndEvents.size != 1) { + logger.warning("Received iCalendar with not exactly one UID; ignoring $fileName") return } + // Event: main VEVENT and potentially attached exceptions (further VEVENTs with RECURRENCE-ID) + val event = uidsAndEvents.values.first() - if (events.size == 1) { - val event = events.first() - - // set default reminder for non-full-day events, if requested - val defaultAlarmMinBefore = accountSettings.getDefaultAlarm() - if (defaultAlarmMinBefore != null && DateUtils.isDateTime(event.dtStart) && event.alarms.isEmpty()) { - val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong())).apply { - // Sets METHOD_ALERT instead of METHOD_DEFAULT in the calendar provider. - // Needed for calendars to actually show a notification. - properties += Action.DISPLAY - } - logger.log(Level.FINE, "${event.uid}: Adding default alarm", alarm) - event.alarms += alarm + val defaultAlarmMinBefore = accountSettings.getDefaultAlarm() + val mainEvent = event.main + if (mainEvent != null && defaultAlarmMinBefore != null && DateUtils.isDateTime(mainEvent.startDate) && mainEvent.alarms.isEmpty()) { + val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong())).apply { + // Sets METHOD_ALERT instead of METHOD_DEFAULT in the calendar provider. + // Needed for calendars to actually show a notification. + properties += Action.DISPLAY } + logger.log(Level.FINE, "${mainEvent.uid}: Adding default alarm", alarm) + mainEvent.components += alarm + } - // update local event, if it exists - val local = localCollection.findByName(fileName) + // map AssociatedEvents (VEVENTs) to EventAndExceptions (Android events) + val androidEvent = AndroidEventBuilder( + calendar = localCollection.androidCalendar, + syncId = fileName, + eTag = eTag, + scheduleTag = scheduleTag, + flags = LocalResource.FLAG_REMOTELY_PRESENT + ).build(event) + + // update local event, if it exists + val local = localCollection.findByName(fileName) + if (local != null) { SyncException.wrapWithLocalResource(local) { - if (local != null) { - logger.log(Level.INFO, "Updating $fileName in local calendar", event) - local.update( - data = event, - fileName = fileName, - eTag = eTag, - scheduleTag = scheduleTag, - flags = LocalResource.FLAG_REMOTELY_PRESENT - ) - } else { - logger.log(Level.INFO, "Adding $fileName to local calendar", event) - localCollection.add( - event = event, - fileName = fileName, - eTag = eTag, - scheduleTag = scheduleTag, - flags = LocalResource.FLAG_REMOTELY_PRESENT - ) - } + logger.log(Level.INFO, "Updating $fileName in local calendar", event) + local.update(androidEvent) } - } else - logger.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName") + } else { + logger.log(Level.INFO, "Adding $fileName to local calendar", event) + localCollection.add(androidEvent) + } } override fun notifyInvalidResourceTitle(): String = diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt index 0a221f6ab..fe53601dc 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt @@ -277,12 +277,15 @@ class ContactsSyncManager @AssistedInject constructor( is LocalGroup -> resource.getContact() else -> throw IllegalArgumentException("resource must be LocalContact or LocalGroup") } - logger.log(Level.FINE, "Preparing upload of vCard ${resource.fileName}", contact) + logger.log(Level.FINE, "Preparing upload of vCard #${resource.id}", contact) // get/create UID val (uid, uidIsGenerated) = DavUtils.generateUidIfNecessary(contact.uid) - if (uidIsGenerated) + if (uidIsGenerated) { + // modify in Contact and persist to contacts provider contact.uid = uid + resource.updateUid(uid) + } // generate vCard and convert to request body val os = ByteArrayOutputStream() @@ -304,10 +307,7 @@ class ContactsSyncManager @AssistedInject constructor( return GeneratedResource( suggestedFileName = DavUtils.fileNameFromUid(uid, "vcf"), - requestBody = os.toByteArray().toRequestBody(mimeType), - onSuccessContext = GeneratedResource.OnSuccessContext( - uid = if (uidIsGenerated) Optional.of(uid) else Optional.empty() - ) + requestBody = os.toByteArray().toRequestBody(mimeType) ) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/GeneratedResource.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/GeneratedResource.kt index c899a4cfe..ba436c5b6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/GeneratedResource.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/GeneratedResource.kt @@ -5,30 +5,28 @@ package at.bitfire.davdroid.sync import okhttp3.RequestBody -import java.util.Optional /** * Represents a resource that has been generated for the purpose of being uploaded. * * @param suggestedFileName file name that can be used for uploading if there's no existing name * @param requestBody resource body (including MIME type) - * @param onSuccessContext context that must be passed to [SyncManager.onSuccessfulUpload] on successful upload + * @param onSuccessContext context that must be passed to [SyncManager.onSuccessfulUpload] + * on successful upload in order to persist the changes made during mapping */ class GeneratedResource( val suggestedFileName: String, val requestBody: RequestBody, - val onSuccessContext: OnSuccessContext + val onSuccessContext: OnSuccessContext? = null ) { /** * Contains information that has been created for a [GeneratedResource], but has not been saved yet. * - * @param uid new UID to persist on successful upload (empty: UID not modified) - * @param sequence new SEQUENCE to persist on successful upload (empty: SEQUENCE not modified) + * @param sequence new SEQUENCE to persist on successful upload (*null*: SEQUENCE not modified) */ data class OnSuccessContext( - val uid: Optional = Optional.empty(), - val sequence: Optional = Optional.empty() + val sequence: Int? = null ) } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt index c9b9455ae..4f3157028 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt @@ -34,6 +34,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.runInterruptible +import net.fortuna.ical4j.model.property.ProdId import okhttp3.HttpUrl import okhttp3.RequestBody.Companion.toRequestBody import java.io.ByteArrayOutputStream @@ -97,15 +98,14 @@ class JtxSyncManager @AssistedInject constructor( } override fun generateUpload(resource: LocalJtxICalObject): GeneratedResource { - logger.log(Level.FINE, "Preparing upload of icalobject ${resource.uid}") + logger.log(Level.FINE, "Preparing upload of icalobject #${resource.id}") val os = ByteArrayOutputStream() - resource.write(os, Constants.iCalProdId) + resource.write(os, ProdId(Constants.iCalProdId)) return GeneratedResource( suggestedFileName = DavUtils.fileNameFromUid(resource.uid, "ics"), - requestBody = os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8), - onSuccessContext = GeneratedResource.OnSuccessContext() // nothing special to update after upload + requestBody = os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8) ) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt index cf6a9d97a..d15607fbb 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt @@ -506,7 +506,7 @@ abstract class SyncManager