diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt index a1a9ebb5e..74fa4daae 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid +import androidx.test.filters.SdkSuppress import at.bitfire.davdroid.network.HttpClient import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -29,6 +30,7 @@ class OkhttpClientTest { @Test + @SdkSuppress(maxSdkVersion = 34) fun testIcloudWithSettings() { httpClientBuilder.build().use { client -> client.okHttpClient diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt index c701dc1e0..df9bc95dc 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/Android10ResolverTest.kt @@ -19,7 +19,7 @@ class Android10ResolverTest { val FQDN_DAVX5 = "www.davx5.com" @Test - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q, maxSdkVersion = 34) fun testResolveA() { val www = InetAddress.getAllByName(FQDN_DAVX5).filterIsInstance().first() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt index b301ad3f8..4579bd61b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -204,10 +204,12 @@ open class LocalAddressBook @AssistedInject constructor( .newUpdate(groupsSyncUri()) .withSelection(Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?", arrayOf(oldAccount.name, oldAccount.type)) .withValue(Groups.ACCOUNT_NAME, newAccount.name) + .withValue(Groups.ACCOUNT_TYPE, newAccount.type) batch += BatchOperation.CpoBuilder .newUpdate(rawContactsSyncUri()) .withSelection(RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?", arrayOf(oldAccount.name, oldAccount.type)) .withValue(RawContacts.ACCOUNT_NAME, newAccount.name) + .withValue(RawContacts.ACCOUNT_TYPE, newAccount.type) batch.commit() // update AndroidAddressBook.account 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 a7cb2c5aa..73715d8e5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt @@ -7,13 +7,13 @@ package at.bitfire.davdroid.resource import android.accounts.Account import android.content.ContentProviderClient import android.content.ContentUris -import android.content.ContentValues import android.provider.CalendarContract.Calendars import android.provider.CalendarContract.Events import androidx.core.content.contentValuesOf import at.bitfire.davdroid.db.SyncState import at.bitfire.ical4android.AndroidCalendar import at.bitfire.ical4android.AndroidCalendarFactory +import at.bitfire.ical4android.AndroidEvent import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.synctools.storage.BatchOperation import at.bitfire.synctools.storage.CalendarBatchOperation @@ -32,14 +32,9 @@ class LocalCalendar private constructor( id: Long ): AndroidCalendar(account, provider, LocalEvent.Factory, id), LocalCollection { - companion object { + private val logger: Logger + get() = Logger.getLogger(javaClass.name) - private const val COLUMN_SYNC_STATE = Calendars.CAL_SYNC1 - - private val logger: Logger - get() = Logger.getGlobal() - - } override val dbCollectionId: Long? get() = syncId?.toLongOrNull() @@ -50,29 +45,16 @@ class LocalCalendar private constructor( override val title: String get() = displayName ?: id.toString() - private var accessLevel: Int = Calendars.CAL_ACCESS_OWNER // assume full access if not specified override val readOnly - get() = accessLevel <= Calendars.CAL_ACCESS_READ + get() = accessLevel?.let { it <= Calendars.CAL_ACCESS_READ } ?: false override var lastSyncState: SyncState? - get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor -> - if (cursor.moveToNext()) - return SyncState.fromString(cursor.getString(0)) - else - null - } + get() = readSyncState()?.let { SyncState.fromString(it) } set(state) { - val values = contentValuesOf(COLUMN_SYNC_STATE to state.toString()) - provider.update(calendarSyncURI(), values, null, null) + writeSyncState(state.toString()) } - override fun populate(info: ContentValues) { - super.populate(info) - accessLevel = info.getAsInteger(Calendars.CALENDAR_ACCESS_LEVEL) ?: Calendars.CAL_ACCESS_OWNER - } - - override fun findDeleted() = queryEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null) @@ -112,7 +94,7 @@ class LocalCalendar private constructor( override fun markNotDirty(flags: Int): Int { - val values = contentValuesOf(LocalEvent.COLUMN_FLAGS to flags) + val values = contentValuesOf(AndroidEvent.COLUMN_FLAGS to flags) return provider.update(Events.CONTENT_URI.asSyncAdapter(account), values, "${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", arrayOf(id.toString())) @@ -122,7 +104,7 @@ class LocalCalendar private constructor( var deleted = 0 // list all non-dirty events with the given flags and delete every row + its exceptions provider.query(Events.CONTENT_URI.asSyncAdapter(account), arrayOf(Events._ID), - "${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${LocalEvent.COLUMN_FLAGS}=?", + "${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${AndroidEvent.COLUMN_FLAGS}=?", arrayOf(id.toString(), flags.toString()), null)?.use { cursor -> val batch = CalendarBatchOperation(provider) while (cursor.moveToNext()) { @@ -138,7 +120,7 @@ class LocalCalendar private constructor( } override fun forgetETags() { - val values = contentValuesOf(LocalEvent.COLUMN_ETAG to null) + val values = contentValuesOf(AndroidEvent.COLUMN_ETAG to null) provider.update(Events.CONTENT_URI.asSyncAdapter(account), values, "${Events.CALENDAR_ID}=?", arrayOf(id.toString())) } @@ -149,7 +131,7 @@ class LocalCalendar private constructor( logger.info("Processing deleted exceptions") provider.query( Events.CONTENT_URI.asSyncAdapter(account), - arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE), + arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE), "${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL", arrayOf(id.toString()), null)?.use { cursor -> while (cursor.moveToNext()) { @@ -162,7 +144,7 @@ class LocalCalendar private constructor( // get original event's SEQUENCE provider.query( ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(account), - arrayOf(LocalEvent.COLUMN_SEQUENCE), + arrayOf(AndroidEvent.COLUMN_SEQUENCE), null, null, null)?.use { cursor2 -> if (cursor2.moveToNext()) { // original event is available @@ -171,7 +153,7 @@ class LocalCalendar private constructor( // re-schedule original event and set it to DIRTY batch += BatchOperation.CpoBuilder .newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(account)) - .withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1) + .withValue(AndroidEvent.COLUMN_SEQUENCE, originalSequence + 1) .withValue(Events.DIRTY, 1) } } @@ -186,7 +168,7 @@ class LocalCalendar private constructor( logger.info("Processing dirty exceptions") provider.query( Events.CONTENT_URI.asSyncAdapter(account), - arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE), + arrayOf(Events._ID, Events.ORIGINAL_ID, AndroidEvent.COLUMN_SEQUENCE), "${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL", arrayOf(id.toString()), null)?.use { cursor -> while (cursor.moveToNext()) { @@ -203,7 +185,7 @@ class LocalCalendar private constructor( // increase SEQUENCE and set DIRTY to 0 batch += BatchOperation.CpoBuilder .newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(account)) - .withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1) + .withValue(AndroidEvent.COLUMN_SEQUENCE, sequence + 1) .withValue(Events.DIRTY, 0) batch.commit() } 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 b09b0c316..690536147 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt @@ -20,22 +20,103 @@ import at.bitfire.ical4android.Event import at.bitfire.ical4android.ICalendar import at.bitfire.ical4android.ical4jVersion import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter -import at.bitfire.synctools.storage.BatchOperation import net.fortuna.ical4j.model.property.ProdId import java.util.UUID -class LocalEvent: AndroidEvent, LocalResource { +class LocalEvent : AndroidEvent, LocalResource { + + override var fileName: String? + get() = syncId + private set(value) { + syncId = value + } + + val weAreOrganizer + get() = event!!.isOrganizer == true + + + constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int) + : super(calendar, event, fileName, eTag, scheduleTag, flags) + + private constructor(calendar: AndroidCalendar<*>, values: ContentValues) + : super(calendar, values) + + + /** + * Creates and sets a new UID in the calendar provider, if no UID is already set. + * It also returns the desired file name for the event for further processing in the sync algorithm. + * + * @return file name to use at upload + */ + override fun prepareForUpload(): String { + // make sure that UID is set + val uid: String = event!!.uid ?: run { + // generate new UID + val newUid = UUID.randomUUID().toString() + + // update in calendar provider + val values = contentValuesOf(Events.UID_2445 to newUid) + calendar.provider.update(eventSyncURI(), values, null, null) + + // update this event + event?.uid = newUid + + newUid + } + + val uidIsGoodFilename = uid.all { char -> + // see RFC 2396 2.2 + char.isLetterOrDigit() || arrayOf( // allow letters and digits + ';', ':', '@', '&', '=', '+', '$', ',', // allow reserved characters except '/' and '?' + '-', '_', '.', '!', '~', '*', '\'', '(', ')' // allow unreserved characters + ).contains(char) + } + return if (uidIsGoodFilename) + "$uid.ics" // use UID as file name + else + "${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(COLUMN_ETAG, eTag) + values.put(COLUMN_SCHEDULE_TAG, scheduleTag) + values.put(COLUMN_SEQUENCE, event!!.sequence) + values.put(Events.DIRTY, 0) + calendar.provider.update(eventSyncURI(), values, null, null) + + if (fileName != null) + this.fileName = fileName + this.eTag = eTag + this.scheduleTag = scheduleTag + } + + override fun updateFlags(flags: Int) { + val values = contentValuesOf(COLUMN_FLAGS to flags) + calendar.provider.update(eventSyncURI(), values, null, null) + + this.flags = flags + } + + override fun resetDeleted() { + val values = contentValuesOf(Events.DELETED to 0) + calendar.provider.update(eventSyncURI(), values, null, null) + } + + object Factory : AndroidEventFactory { + override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) = + LocalEvent(calendar, values) + } + companion object { init { ICalendar.prodId = ProdId("DAVx5/${BuildConfig.VERSION_NAME} ical4j/" + ical4jVersion) } - const val COLUMN_ETAG = Events.SYNC_DATA1 - const val COLUMN_FLAGS = Events.SYNC_DATA2 - const val COLUMN_SEQUENCE = Events.SYNC_DATA3 - const val COLUMN_SCHEDULE_TAG = Events.SYNC_DATA4 - /** * Marks the event as deleted * @param eventID @@ -126,7 +207,7 @@ class LocalEvent: AndroidEvent, LocalResource { val exceptionInstances = numDirectInstances(provider, account, exceptionEventID) if (exceptionInstances == null) - // number of instances of exception can't be determined; so the total number of instances is also unclear + // number of instances of exception can't be determined; so the total number of instances is also unclear return null numInstances += exceptionInstances @@ -136,134 +217,4 @@ class LocalEvent: AndroidEvent, LocalResource { } } - - override var fileName: String? = null - private set - - override var eTag: String? = null - override var scheduleTag: String? = null - - override var flags: Int = 0 - private set - - var weAreOrganizer = false - private set - - - constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int): super(calendar, event) { - this.fileName = fileName - this.eTag = eTag - this.scheduleTag = scheduleTag - this.flags = flags - } - - private constructor(calendar: AndroidCalendar<*>, values: ContentValues): super(calendar, values) { - fileName = values.getAsString(Events._SYNC_ID) - eTag = values.getAsString(COLUMN_ETAG) - scheduleTag = values.getAsString(COLUMN_SCHEDULE_TAG) - flags = values.getAsInteger(COLUMN_FLAGS) ?: 0 - } - - override fun populateEvent(row: ContentValues, groupScheduled: Boolean) { - val event = requireNotNull(event) - event.sequence = row.getAsInteger(COLUMN_SEQUENCE) - - val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER) - weAreOrganizer = isOrganizer != null && isOrganizer != 0 - - super.populateEvent(row, groupScheduled) - } - - override fun buildEvent(recurrence: Event?, builder: BatchOperation.CpoBuilder) { - val event = requireNotNull(event) - - val buildException = recurrence != null - val eventToBuild = recurrence ?: event - - builder .withValue(COLUMN_SEQUENCE, eventToBuild.sequence) - .withValue(Events.DIRTY, 0) - .withValue(Events.DELETED, 0) - .withValue(COLUMN_FLAGS, flags) - - if (buildException) - builder .withValue(Events.ORIGINAL_SYNC_ID, fileName) - else - builder .withValue(Events._SYNC_ID, fileName) - .withValue(COLUMN_ETAG, eTag) - .withValue(COLUMN_SCHEDULE_TAG, scheduleTag) - - super.buildEvent(recurrence, builder) - } - - - /** - * Creates and sets a new UID in the calendar provider, if no UID is already set. - * It also returns the desired file name for the event for further processing in the sync algorithm. - * - * @return file name to use at upload - */ - override fun prepareForUpload(): String { - // make sure that UID is set - val uid: String = event!!.uid ?: run { - // generate new UID - val newUid = UUID.randomUUID().toString() - - // update in calendar provider - val values = contentValuesOf(Events.UID_2445 to newUid) - calendar.provider.update(eventSyncURI(), values, null, null) - - // update this event - event?.uid = newUid - - newUid - } - - val uidIsGoodFilename = uid.all { char -> - // see RFC 2396 2.2 - char.isLetterOrDigit() || arrayOf( // allow letters and digits - ';',':','@','&','=','+','$',',', // allow reserved characters except '/' and '?' - '-','_','.','!','~','*','\'','(',')' // allow unreserved characters - ).contains(char) - } - return if (uidIsGoodFilename) - "$uid.ics" // use UID as file name - else - "${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(COLUMN_ETAG, eTag) - values.put(COLUMN_SCHEDULE_TAG, scheduleTag) - values.put(COLUMN_SEQUENCE, event!!.sequence) - values.put(Events.DIRTY, 0) - calendar.provider.update(eventSyncURI(), values, null, null) - - if (fileName != null) - this.fileName = fileName - this.eTag = eTag - this.scheduleTag = scheduleTag - } - - override fun updateFlags(flags: Int) { - val values = contentValuesOf(COLUMN_FLAGS to flags) - calendar.provider.update(eventSyncURI(), values, null, null) - - this.flags = flags - } - - override fun resetDeleted() { - val values = contentValuesOf(Events.DELETED to 0) - calendar.provider.update(eventSyncURI(), values, null, null) - } - - object Factory: AndroidEventFactory { - override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) = - LocalEvent(calendar, values) - } - -} - +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt index c05ff32ec..f0777bf5b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt @@ -110,7 +110,7 @@ class LocalTaskList private constructor( arrayOf(id.toString(), flags.toString())) override fun forgetETags() { - val values = contentValuesOf(LocalEvent.COLUMN_ETAG to null) + val values = contentValuesOf(LocalTask.COLUMN_ETAG to null) provider.update(tasksSyncUri(), values, "${Tasks.LIST_ID}=?", arrayOf(id.toString())) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a53a38dc..41faa276b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ androidx-test-junit = "1.2.1" androidx-work = "2.10.2" bitfire-cert4android = "b67ba86d31" bitfire-dav4jvm = "05fb8ecda6" -bitfire-synctools = "366184ea7b" +bitfire-synctools = "78cacf8eba" compose-accompanist = "0.37.3" compose-bom = "2025.06.01" dnsjava = "3.6.3"