Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit b0609faf authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

Update synctools for `AndroidEvent2` (#1601)



* [WIP] Update synctools

* Refactor LocalEvent to use LegacyAndroidCalendar for event operations

* Refactor LocalEvent to use LegacyAndroidCalendar for event operations

* Update cert4android to get 16 kB page size support over Conscrypt 2.5.3 (#1581)

* Move SyncState to resource package because it's not in the database (#1585)

* Update dependencies, including dav4jvm that updates okhttp to 5.x (#1593)

* Update dependencies, including dav4jvm that updates okhttp to 5.x

* Update mockk and okhttp

* Bump version to 4.5.2-beta.1

* Move Insert/update to DAO (#1587)

* Move homeset insert/update logic from repository to DAO; add thread-safety test

* Rename insertOrUpdateByUrlRememberSync

* Fix exceptions being fetched as `Parcelable` instead of `Serializable` in `DebugInfoActivity` (#1597)

Typo: replace `getParcelableExtra` with `getSerializableExtra`

Signed-off-by: default avatarArnau Mora <arnyminerz@proton.me>

* Bump version to 4.5.2

* Fetch translations from Transifex

* Add documentation and handle missing event in LocalEvent

* Minor changes

* Rename `event` to `getCachedEvent()` to make it more clear what it does

* Update SEQUENCE after successful event upload more explicitly

* Update sequence after successful calendar event upload

* Remove deprecated add() method from LocalResource

* Update KDoc

---------

Signed-off-by: default avatarArnau Mora <arnyminerz@proton.me>
Co-authored-by: default avatarSunik Kupfer <kupfer@bitfire.at>
Co-authored-by: default avatarArnau Mora <arnyminerz@proton.me>
parent 94a85833
Loading
Loading
Loading
Loading
+7 −5
Original line number Diff line number Diff line
@@ -12,8 +12,8 @@ import android.provider.CalendarContract
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
import android.provider.CalendarContract.Events
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.LegacyAndroidCalendar
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import at.bitfire.ical4android.util.MiscUtils.closeCompat
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
@@ -92,8 +92,9 @@ class LocalCalendarTest {
                status = Status.VEVENT_CANCELLED
            })
        }
        val localEvent = AndroidEvent(calendar.androidCalendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
        localEvent.add()
        val legacyCalendar = LegacyAndroidCalendar(calendar.androidCalendar)
        legacyCalendar.add(event = event, syncId = "filename.ics", flags = LocalResource.FLAG_REMOTELY_PRESENT)
        val localEvent = calendar.findByName("filename.ics")!!
        val eventId = localEvent.id!!

        // set event as dirty
@@ -122,8 +123,9 @@ class LocalCalendarTest {
            summary = "Event with 3 instances"
            rRules.add(RRule("FREQ=DAILY;COUNT=3"))
        }
        val localEvent = AndroidEvent(calendar.androidCalendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
        localEvent.add()
        val legacyCalendar = LegacyAndroidCalendar(calendar.androidCalendar)
        legacyCalendar.add(event = event, syncId = "filename.ics", flags = LocalResource.FLAG_REMOTELY_PRESENT)
        val localEvent = calendar.findByName("filename.ics")!!
        val eventId = localEvent.id!!

        // set event as dirty
+17 −11
Original line number Diff line number Diff line
@@ -14,8 +14,8 @@ 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.AndroidEvent
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.LegacyAndroidCalendar
import at.bitfire.ical4android.util.MiscUtils.closeCompat
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
import at.techbee.jtx.JtxContract.asSyncAdapter
@@ -74,8 +74,10 @@ class LocalEventTest {
            dtStart = DtStart("20220120T010203Z")
            summary = "Event without uid"
        }
        val localEvent = LocalEvent(AndroidEvent(calendar.androidCalendar, event, null))
        localEvent.add()    // save it to calendar storage

        val legacyCalendar = LegacyAndroidCalendar(calendar.androidCalendar)
        legacyCalendar.add(event = event, syncId = "filename.ics", flags = LocalResource.FLAG_REMOTELY_PRESENT)
        val localEvent = calendar.findByName("filename.ics")!!

        // prepare for upload - this should generate a new random uuid, returned as filename
        val fileNameWithSuffix = localEvent.prepareForUpload()
@@ -102,8 +104,9 @@ class LocalEventTest {
            summary = "Event with normal uid"
            uid = "some-event@hostname.tld"     // old UID format, UUID would be new format
        }
        val localEvent = LocalEvent(AndroidEvent(calendar.androidCalendar, event, null))
        localEvent.add() // save it to calendar storage
        val legacyCalendar = LegacyAndroidCalendar(calendar.androidCalendar)
        legacyCalendar.add(event = event, syncId = "filename.ics", flags = LocalResource.FLAG_REMOTELY_PRESENT)
        val localEvent = calendar.findByName("filename.ics")!!

        // prepare for upload - this should use the UID for the file name
        val fileNameWithSuffix = localEvent.prepareForUpload()
@@ -129,8 +132,9 @@ class LocalEventTest {
            summary = "Event with funny uid"
            uid = "https://www.example.com/events/asdfewfe-cxyb-ewrws-sadfrwerxyvser-asdfxye-"
        }
        val localEvent = LocalEvent(AndroidEvent(calendar.androidCalendar, event, null))
        localEvent.add() // save it to calendar storage
        val legacyCalendar = LegacyAndroidCalendar(calendar.androidCalendar)
        legacyCalendar.add(event = event, syncId = "filename.ics", flags = LocalResource.FLAG_REMOTELY_PRESENT)
        val localEvent = calendar.findByName("filename.ics")!!

        // prepare for upload - this should generate a new random uuid, returned as filename
        val fileNameWithSuffix = localEvent.prepareForUpload()
@@ -181,8 +185,9 @@ class LocalEventTest {
                status = Status.VEVENT_CANCELLED
            })
        }
        val localEvent = LocalEvent(AndroidEvent(calendar.androidCalendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT))
        localEvent.add()
        val legacyCalendar = LegacyAndroidCalendar(calendar.androidCalendar)
        legacyCalendar.add(event = event, syncId = "filename.ics", flags = LocalResource.FLAG_REMOTELY_PRESENT)
        val localEvent = calendar.findByName("filename.ics")!!
        val eventId = localEvent.id!!

        // set event as dirty
@@ -210,8 +215,9 @@ class LocalEventTest {
            summary = "Event with 3 instances"
            rRules.add(RRule("FREQ=DAILY;COUNT=3"))
        }
        val localEvent = LocalEvent(AndroidEvent(calendar.androidCalendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT))
        localEvent.add()
        val legacyCalendar = LegacyAndroidCalendar(calendar.androidCalendar)
        legacyCalendar.add(event = event, syncId = "filename.ics", flags = LocalResource.FLAG_REMOTELY_PRESENT)
        val localEvent = calendar.findByName("filename.ics")!!
        val eventId = localEvent.id!!

        // set event as dirty
+0 −1
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ class LocalTestResource: LocalResource<Any> {
        this.flags = flags
    }

    override fun add() = throw NotImplementedError()
    override fun update(data: Any) = throw NotImplementedError()
    override fun delete() = throw NotImplementedError()
    override fun resetDeleted() = throw NotImplementedError()
+34 −44
Original line number Diff line number Diff line
@@ -12,12 +12,12 @@ import at.bitfire.ical4android.AndroidEvent
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.AndroidEvent2
import at.bitfire.synctools.storage.calendar.CalendarBatchOperation
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.LinkedList
import java.util.logging.Level
import java.util.logging.Logger

/**
@@ -56,11 +56,15 @@ class LocalCalendar @AssistedInject constructor(
            androidCalendar.writeSyncState(state.toString())
        }


    override fun findDeleted() =
        androidCalendar
            .findEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null)
            .map { LocalEvent(it) }
    override fun findDeleted(): List<LocalEvent> {
        val result = LinkedList<LocalEvent>()
        androidCalendar.iterateEventRows(null, "${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null) { values ->
            // create legacy AndroidEvent from AndroidEvent2's content values
            val legacyEvent = AndroidEvent(androidCalendar, values)
            result += LocalEvent(legacyEvent)
        }
        return result
    }

    override fun findDirty(): List<LocalEvent> {
        val dirty = LinkedList<LocalEvent>()
@@ -70,37 +74,23 @@ class LocalCalendar @AssistedInject constructor(
         * 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.
         */
        for (androidEvent in androidCalendar.findEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null)) {
            val localEvent = LocalEvent(androidEvent)
            try {
                val event = requireNotNull(androidEvent.event)

                val nonGroupScheduled = event.attendees.isEmpty()
                val weAreOrganizer = localEvent.weAreOrganizer

                val sequence = event.sequence
                if (sequence == null)
                    // sequence has not been assigned yet (i.e. this event was just locally created)
                    event.sequence = 0
                else if (nonGroupScheduled || weAreOrganizer)   // increase sequence
                    event.sequence = sequence + 1

            } catch(e: Exception) {
                logger.log(Level.WARNING, "Couldn't check/increase sequence", e)
            }
            dirty += localEvent
        androidCalendar.iterateEventRows(null, "${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null) { values ->
            val legacyEvent = AndroidEvent(androidCalendar, values)
            dirty += LocalEvent(legacyEvent)
        }

        return dirty
    }

    override fun findByName(name: String) =
        androidCalendar.findEvents("${Events._SYNC_ID}=?", arrayOf(name)).firstOrNull()?.let { LocalEvent(it) }

        androidCalendar.findEventRow(null, "${Events._SYNC_ID}=?", arrayOf(name))?.let { values ->
            val legacyEvent = AndroidEvent(androidCalendar, values)
            LocalEvent(legacyEvent)
        }

    override fun markNotDirty(flags: Int) =
        androidCalendar.updateEvents(
            contentValuesOf(AndroidEvent.COLUMN_FLAGS to flags),
        androidCalendar.updateEventRows(
            contentValuesOf(AndroidEvent2.COLUMN_FLAGS to flags),
            "${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL",
            arrayOf(androidCalendar.id.toString())
        )
@@ -108,9 +98,9 @@ class LocalCalendar @AssistedInject constructor(
    override fun removeNotDirtyMarked(flags: Int): Int {
        // list all non-dirty events with the given flags and delete every row + its exceptions
        val batch = CalendarBatchOperation(androidCalendar.client)
        androidCalendar.iterateEvents(
        androidCalendar.iterateEventRows(
            arrayOf(Events._ID),
            "${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${AndroidEvent.COLUMN_FLAGS}=?",
            "${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${AndroidEvent2.COLUMN_FLAGS}=?",
            arrayOf(androidCalendar.id.toString(), flags.toString())
        ) { values ->
            val id = values.getAsInteger(Events._ID)
@@ -124,8 +114,8 @@ class LocalCalendar @AssistedInject constructor(
    }

    override fun forgetETags() {
        androidCalendar.updateEvents(
            contentValuesOf(AndroidEvent.COLUMN_ETAG to null),
        androidCalendar.updateEventRows(
            contentValuesOf(AndroidEvent2.COLUMN_ETAG to null),
            "${Events.CALENDAR_ID}=?", arrayOf(androidCalendar.id.toString())
        )
    }
@@ -135,8 +125,8 @@ class LocalCalendar @AssistedInject constructor(
        // process deleted exceptions
        logger.info("Processing deleted exceptions")

        androidCalendar.iterateEvents(
            arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE),
        androidCalendar.iterateEventRows(
            arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent2.COLUMN_SEQUENCE),
            "${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL",
            arrayOf(androidCalendar.id.toString())
        ) { values ->
@@ -148,12 +138,12 @@ class LocalCalendar @AssistedInject constructor(
            val batch = CalendarBatchOperation(androidCalendar.client)

            // enqueue: increase sequence of main event
            val originalEventValues = androidCalendar.getEventValues(originalID, arrayOf(AndroidEvent.COLUMN_SEQUENCE))
            val originalSequence = originalEventValues?.getAsInteger(AndroidEvent.COLUMN_SEQUENCE) ?: 0
            val originalEventValues = androidCalendar.getEventRow(originalID, arrayOf(AndroidEvent2.COLUMN_SEQUENCE))
            val originalSequence = originalEventValues?.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE) ?: 0

            batch += BatchOperation.CpoBuilder
                .newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(androidCalendar.account))
                .withValue(AndroidEvent.COLUMN_SEQUENCE, originalSequence + 1)
                .withValue(AndroidEvent2.COLUMN_SEQUENCE, originalSequence + 1)
                .withValue(Events.DIRTY, 1)

            // completely remove deleted exception
@@ -163,8 +153,8 @@ class LocalCalendar @AssistedInject constructor(

        // process dirty exceptions
        logger.info("Processing dirty exceptions")
        androidCalendar.iterateEvents(
            arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE),
        androidCalendar.iterateEventRows(
            arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent2.COLUMN_SEQUENCE),
            "${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL",
            arrayOf(androidCalendar.id.toString())
        ) { values ->
@@ -172,7 +162,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(AndroidEvent.COLUMN_SEQUENCE) ?: 0
            val sequence = values.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE) ?: 0

            val batch = CalendarBatchOperation(androidCalendar.client)

@@ -184,7 +174,7 @@ class LocalCalendar @AssistedInject constructor(
            // enqueue: increase exception SEQUENCE and set DIRTY to 0
            batch += BatchOperation.CpoBuilder
                .newUpdate(androidCalendar.eventUri(id))
                .withValue(AndroidEvent.COLUMN_SEQUENCE, sequence + 1)
                .withValue(AndroidEvent2.COLUMN_SEQUENCE, sequence + 1)
                .withValue(Events.DIRTY, 0)

            batch.commit()
@@ -198,7 +188,7 @@ class LocalCalendar @AssistedInject constructor(
     */
    fun deleteDirtyEventsWithoutInstances() {
        // Iterate dirty main events without exceptions
        androidCalendar.iterateEvents(
        androidCalendar.iterateEventRows(
            arrayOf(Events._ID),
            "${Events.DIRTY} AND NOT ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL",
            null
@@ -211,7 +201,7 @@ class LocalCalendar @AssistedInject constructor(
            // delete event if there are no instances
            if (numEventInstances == 0) {
                logger.fine("Marking event #$eventId without instances as deleted")
                androidCalendar.updateEvent(eventId, contentValuesOf(Events.DELETED to 1))
                androidCalendar.updateEventRow(eventId, contentValuesOf(Events.DELETED to 1))
            }
        }
    }
+68 −13
Original line number Diff line number Diff line
@@ -9,6 +9,9 @@ import android.provider.CalendarContract.Events
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.LegacyAndroidCalendar
import at.bitfire.synctools.storage.LocalStorageException
import at.bitfire.synctools.storage.calendar.AndroidEvent2
import java.util.UUID

class LocalEvent(
@@ -37,8 +40,6 @@ class LocalEvent(
    override val flags: Int
        get() = androidEvent.flags

    override fun add() = androidEvent.add()

    override fun update(data: Event) = androidEvent.update(data)

    override fun delete() = androidEvent.delete()
@@ -46,8 +47,64 @@ class LocalEvent(

    // other methods

    val weAreOrganizer
        get() = androidEvent.event!!.isOrganizer == true
    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
    }

    /**
     * Generates the [Event] that should actually be uploaded:
     *
     * 1. Takes the [getCachedEvent].
     * 2. Calculates the new SEQUENCE.
     *
     * _Note: This method currently modifies the object returned by [getCachedEvent], but
     * this may change in the future._
     *
     * @return data object that should be used for uploading
     */
    fun eventToUpload(): Event {
        val event = getCachedEvent()

        val nonGroupScheduled = event.attendees.isEmpty()
        val weAreOrganizer = event.isOrganizer == true

        // Increase sequence (event.sequence null/non-null behavior is defined by the Event, see KDoc of event.sequence):
        // - 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 sequence = event.sequence
        if (sequence != null && (nonGroupScheduled || weAreOrganizer))
            event.sequence = sequence + 1

        return event
    }

    /**
     * Updates the SEQUENCE of the event in the content provider.
     *
     * @param sequence  new sequence value
     */
    fun updateSequence(sequence: Int?) {
        androidEvent.update(contentValuesOf(
            AndroidEvent2.COLUMN_SEQUENCE to sequence
        ))
    }


    /**
@@ -58,16 +115,16 @@ class LocalEvent(
     */
    override fun prepareForUpload(): String {
        // make sure that UID is set
        val uid: String = androidEvent.event!!.uid ?: run {
        val uid: String = getCachedEvent().uid ?: run {
            // generate new UID
            val newUid = UUID.randomUUID().toString()

            // update in calendar provider
            // persist to calendar provider
            val values = contentValuesOf(Events.UID_2445 to newUid)
            androidEvent.update(values)

            // update this event
            androidEvent.event?.uid = newUid
            // update in cached event data object
            getCachedEvent().uid = newUid

            newUid
        }
@@ -85,14 +142,12 @@ class LocalEvent(
            "${UUID.randomUUID()}.ics"      // UID would be dangerous as file name, use random UUID instead
    }


    override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
        val values = ContentValues(5)
        if (fileName != null)
            values.put(Events._SYNC_ID, fileName)
        values.put(AndroidEvent.COLUMN_ETAG, eTag)
        values.put(AndroidEvent.COLUMN_SCHEDULE_TAG, scheduleTag)
        values.put(AndroidEvent.COLUMN_SEQUENCE, androidEvent.event!!.sequence)
        values.put(AndroidEvent2.COLUMN_ETAG, eTag)
        values.put(AndroidEvent2.COLUMN_SCHEDULE_TAG, scheduleTag)
        values.put(Events.DIRTY, 0)
        androidEvent.update(values)

@@ -103,7 +158,7 @@ class LocalEvent(
    }

    override fun updateFlags(flags: Int) {
        val values = contentValuesOf(AndroidEvent.COLUMN_FLAGS to flags)
        val values = contentValuesOf(AndroidEvent2.COLUMN_FLAGS to flags)
        androidEvent.update(values)

        androidEvent.flags = flags
Loading