Update synctools (#1579)

* [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: Arnau Mora <arnyminerz@proton.me>

* Bump version to 4.5.2

* Fetch translations from Transifex

* Add documentation and handle missing event in LocalEvent

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Arnau Mora <arnyminerz@proton.me>
This commit is contained in:
Ricki Hirner
2025-07-21 11:54:20 +02:00
committed by GitHub
parent 71f3558b4b
commit 62a0ba3520
8 changed files with 96 additions and 64 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -12,6 +12,7 @@ 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
@@ -56,11 +57,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,10 +75,11 @@ 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)
androidCalendar.iterateEventRows(null, "${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null) { values ->
val legacyEvent = AndroidEvent(androidCalendar, values)
val localEvent = LocalEvent(legacyEvent)
try {
val event = requireNotNull(androidEvent.event)
val event = localEvent.event
val nonGroupScheduled = event.attendees.isEmpty()
val weAreOrganizer = localEvent.weAreOrganizer
@@ -95,12 +101,14 @@ class LocalCalendar @AssistedInject constructor(
}
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 +116,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 +132,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 +143,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 +156,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 +171,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 +180,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 +192,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 +206,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 +219,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))
}
}
}

View File

@@ -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,12 @@ class LocalEvent(
// other methods
val weAreOrganizer
get() = androidEvent.event!!.isOrganizer == true
val event: Event by lazy {
val legacyCalendar = LegacyAndroidCalendar(androidEvent.calendar)
legacyCalendar.getEvent(androidEvent.id) ?: throw LocalStorageException("Event ${androidEvent.id} not found")
}
val weAreOrganizer: Boolean = event.isOrganizer == true
/**
@@ -58,7 +63,7 @@ class LocalEvent(
*/
override fun prepareForUpload(): String {
// make sure that UID is set
val uid: String = androidEvent.event!!.uid ?: run {
val uid: String = event.uid ?: run {
// generate new UID
val newUid = UUID.randomUUID().toString()
@@ -66,8 +71,8 @@ class LocalEvent(
val values = contentValuesOf(Events.UID_2445 to newUid)
androidEvent.update(values)
// update this event
androidEvent.event?.uid = newUid
// update in event data object (does not write to calendar store!)
event.uid = newUid
newUid
}
@@ -85,14 +90,16 @@ class LocalEvent(
"${UUID.randomUUID()}.ics" // UID would be dangerous as file name, use random UUID instead
}
@Deprecated("Use add...() of specific collection implementation", level = DeprecationLevel.ERROR)
override fun add() = throw NotImplementedError()
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(AndroidEvent2.COLUMN_SEQUENCE, event.sequence)
values.put(Events.DIRTY, 0)
androidEvent.update(values)
@@ -103,7 +110,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

View File

@@ -5,7 +5,11 @@
package at.bitfire.davdroid.resource
import android.net.Uri
import at.bitfire.davdroid.resource.LocalResource.Companion.FLAG_REMOTELY_PRESENT
/**
* Defines operations that are used by SyncManager for all sync data types.
*/
interface LocalResource<in TData: Any> {
companion object {
@@ -74,6 +78,7 @@ interface LocalResource<in TData: Any> {
*
* @return content URI of the created row (e.g. event URI)
*/
@Deprecated("Use add...() of specific collection implementation")
fun add(): Uri
/**

View File

@@ -12,8 +12,8 @@ import android.provider.CalendarContract
import android.util.Base64
import androidx.core.content.ContextCompat
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.UnknownProperty
import at.bitfire.synctools.storage.calendar.AndroidEvent2
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 AndroidEvent.EXTNAME_URL,
CalendarContract.ExtendedProperties.NAME to AndroidEvent2.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 ${AndroidEvent.EXTNAME_URL}",
"Couldn't rewrite URL from unknown property to ${AndroidEvent2.EXTNAME_URL}",
e
)
}

View File

@@ -28,10 +28,10 @@ import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.resource.SyncState
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.EventReader
import at.bitfire.ical4android.EventWriter
import at.bitfire.ical4android.LegacyAndroidCalendar
import at.bitfire.ical4android.util.DateUtils
import at.bitfire.synctools.exception.InvalidRemoteResourceException
import dagger.assisted.Assisted
@@ -180,7 +180,7 @@ class CalendarSyncManager @AssistedInject constructor(
override fun generateUpload(resource: LocalEvent): RequestBody =
SyncException.wrapWithLocalResource(resource) {
val event = requireNotNull(resource.androidEvent.event)
val event = resource.event
logger.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event)
// write iCalendar to string and convert to request body
@@ -289,10 +289,14 @@ class CalendarSyncManager @AssistedInject constructor(
local.update(event)
} else {
logger.log(Level.INFO, "Adding $fileName to local calendar", event)
val newLocal = LocalEvent(AndroidEvent(localCollection.androidCalendar, event, fileName, eTag, scheduleTag, LocalResource.FLAG_REMOTELY_PRESENT))
SyncException.wrapWithLocalResource(newLocal) {
newLocal.add()
}
val legacyCalendar = LegacyAndroidCalendar(localCollection.androidCalendar)
legacyCalendar.add(
event = event,
syncId = fileName,
eTag = eTag,
scheduleTag = scheduleTag,
flags = LocalResource.FLAG_REMOTELY_PRESENT
)
}
}
} else

View File

@@ -20,7 +20,7 @@ androidx-test-junit = "1.2.1"
androidx-work = "2.10.2"
bitfire-cert4android = "41009d48ed"
bitfire-dav4jvm = "fda822904a"
bitfire-synctools = "503324b4fe"
bitfire-synctools = "b9f83dd8e9"
compose-accompanist = "0.37.3"
compose-bom = "2025.07.00"
dnsjava = "3.6.3"