Skip to content

Don't upload event when calendar is read only #587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class LocalTestCollection: LocalCollection<LocalTestResource> {

val entries = mutableListOf<LocalTestResource>()

override val readOnly: Boolean
get() = throw NotImplementedError()

override fun findDeleted() = entries.filter { it.deleted }
override fun findDirty() = entries.filter { it.dirty }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ class LocalTestResource: LocalResource<Any> {
override fun add() = throw NotImplementedError()
override fun update(data: Any) = throw NotImplementedError()
override fun delete() = throw NotImplementedError()
override fun resetDeleted() = throw NotImplementedError()

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,4 @@ package at.bitfire.davdroid.resource

import at.bitfire.vcard4android.Contact

interface LocalAddress: LocalResource<Contact> {

fun resetDeleted()

}
interface LocalAddress: LocalResource<Contact>
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ 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

override var lastSyncState: SyncState?
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
Expand All @@ -105,6 +109,11 @@ class LocalCalendar private constructor(
}


override fun populate(info: ContentValues) {
super.populate(info)
accessLevel = info.getAsInteger(Calendars.CALENDAR_ACCESS_LEVEL) ?: Calendars.CAL_ACCESS_OWNER
}

fun update(info: Collection, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ interface LocalCollection<out T: LocalResource<*>> {

var lastSyncState: SyncState?

/**
* Whether the collection should be treated as read-only on sync.
* Stops uploading dirty events (Server side changes are still downloaded).
*/
val readOnly: Boolean

/**
* Finds local resources of this collection which have been marked as *deleted* by the user
* or an app acting on their behalf.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import at.bitfire.ical4android.Ical4Android
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import net.fortuna.ical4j.model.property.ProdId
import java.util.UUID

class LocalEvent: AndroidEvent, LocalResource<Event> {

companion object {
Expand Down Expand Up @@ -51,6 +50,7 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
)
}


/**
* Finds the amount of direct instances this event has (without exceptions); used by [numInstances]
* to find the number of instances of exceptions.
Expand Down Expand Up @@ -256,10 +256,15 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
this.flags = flags
}

override fun resetDeleted() {
val values = ContentValues(1).apply { put(Events.DELETED, 0) }
calendar.provider.update(eventSyncURI(), values, null, null)
}

object Factory: AndroidEventFactory<LocalEvent> {
override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) =
LocalEvent(calendar, values)
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Lo
}
}

override val readOnly: Boolean
get() = throw NotImplementedError()

override val tag: String
get() = "jtx-${account.name}-$id"
override val title: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,9 @@ class LocalJtxICalObject(
}

}

override fun resetDeleted() {
throw NotImplementedError()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,9 @@ interface LocalResource<in TData: Any> {
*/
fun delete(): Int

/**
* Undoes deletion of the data object from the content provider.
*/
fun resetDeleted()

}
4 changes: 4 additions & 0 deletions app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class LocalTask: DmfsTask, LocalResource<Task> {
this.flags = flags
}

override fun resetDeleted() {
throw NotImplementedError()
}


object Factory: DmfsTaskFactory<LocalTask> {
override fun fromProvider(taskList: DmfsTaskList<*>, values: ContentValues) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class LocalTaskList private constructor(

}

private var accessLevel: Int = TaskListColumns.ACCESS_LEVEL_UNDEFINED
override val readOnly
get() =
accessLevel != TaskListColumns.ACCESS_LEVEL_UNDEFINED &&
accessLevel <= TaskListColumns.ACCESS_LEVEL_READ

override val tag: String
get() = "tasks-${account.name}-$id"

Expand Down Expand Up @@ -96,8 +102,13 @@ class LocalTaskList private constructor(
}


override fun populate(values: ContentValues) {
super.populate(values)
accessLevel = values.getAsInteger(TaskListColumns.ACCESS_LEVEL)
}

fun update(info: Collection, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))
update(valuesFromCollectionInfo(info, updateColor))


override fun findDeleted() = queryTasks(Tasks._DELETED, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,29 +73,74 @@ class CalendarSyncManager(
}

override fun queryCapabilities(): SyncState? =
remoteExceptionContext {
var syncState: SyncState? = null
it.propfind(0, MaxResourceSize.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[MaxResourceSize::class.java]?.maxSize?.let { maxSize ->
Logger.log.info("Calendar accepts events up to ${FileUtils.byteCountToDisplaySize(maxSize)}")
}

response[SupportedReportSet::class.java]?.let { supported ->
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState = syncState(response)
remoteExceptionContext {
var syncState: SyncState? = null
it.propfind(0, MaxResourceSize.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[MaxResourceSize::class.java]?.maxSize?.let { maxSize ->
Logger.log.info("Calendar accepts events up to ${FileUtils.byteCountToDisplaySize(maxSize)}")
}

response[SupportedReportSet::class.java]?.let { supported ->
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState = syncState(response)
}
}

Logger.log.info("Calendar supports Collection Sync: $hasCollectionSync")
syncState
Logger.log.info("Calendar supports Collection Sync: $hasCollectionSync")
syncState
}

override fun syncAlgorithm() =
if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
SyncAlgorithm.PROPFIND_REPORT
else
SyncAlgorithm.COLLECTION_SYNC

override fun processLocallyDeleted(): Boolean {
if (localCollection.readOnly) {
var modified = false
for (event in localCollection.findDeleted()) {
Logger.log.warning("Restoring locally deleted event (read-only calendar!)")
localExceptionContext(event) { it.resetDeleted() }
modified = true
}

override fun syncAlgorithm() = if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
SyncAlgorithm.PROPFIND_REPORT
else
SyncAlgorithm.COLLECTION_SYNC
// This is unfortunately ugly: When an event has been inserted to a read-only calendar
// it's not enough to force synchronization (by returning true),
// but we also need to make sure all events are downloaded again.
if (modified)
localCollection.lastSyncState = null

return modified
}
// mirror deletions to remote collection (DELETE)
return super.processLocallyDeleted()
}

override fun uploadDirty(): Boolean {
var modified = false
if (localCollection.readOnly) {
for (event in localCollection.findDirty()) {
Logger.log.warning("Resetting locally modified event to ETag=null (read-only calendar!)")
localExceptionContext(event) { it.clearDirty(null, null) }
modified = true
}

// This is unfortunately ugly: When an event has been inserted to a read-only calendar
// it's not enough to force synchronization (by returning true),
// but we also need to make sure all events are downloaded again.
if (modified)
localCollection.lastSyncState = null
}

// generate UID/file name for newly created events
val superModified = super.uploadDirty()

// return true when any operation returned true
return modified or superModified
}

override fun generateUpload(resource: LocalEvent): RequestBody = localExceptionContext(resource) {
val event = requireNotNull(resource.event)
Expand Down Expand Up @@ -143,8 +188,7 @@ class CalendarSyncManager(
}
}

override fun postProcess() {
}
override fun postProcess() {}


// helpers
Expand Down Expand Up @@ -195,6 +239,6 @@ class CalendarSyncManager(
}

override fun notifyInvalidResourceTitle(): String =
context.getString(R.string.sync_invalid_event)
context.getString(R.string.sync_invalid_event)

}
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ class ContactsSyncManager(
infix fun <T> Set<T>.disjunct(other: Set<T>) = (this - other) union (other - this)
}

private val readOnly = localAddressBook.readOnly

private var hasVCard4 = false
private var hasJCard = false
private val groupStrategy = when (accountSettings.getGroupMethod()) {
Expand Down Expand Up @@ -177,7 +175,7 @@ class ContactsSyncManager(
SyncAlgorithm.PROPFIND_REPORT

override fun processLocallyDeleted() =
if (readOnly) {
if (localCollection.readOnly) {
var modified = false
for (group in localCollection.findDeletedGroups()) {
Logger.log.warning("Restoring locally deleted group (read-only address book!)")
Expand Down Expand Up @@ -205,7 +203,7 @@ class ContactsSyncManager(
override fun uploadDirty(): Boolean {
var modified = false

if (readOnly) {
if (localCollection.readOnly) {
for (group in localCollection.findDirtyGroups()) {
Logger.log.warning("Resetting locally modified group to ETag=null (read-only address book!)")
localExceptionContext(group) { it.clearDirty(null, null) }
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ androidx-work = "2.9.0"
appIntro = "7.0.0-beta02"
bitfire-cert4android = "f0964cb"
bitfire-dav4jvm = "b30913f"
bitfire-ical4android = "998f6b6"
bitfire-ical4android = "31650d1"
bitfire-vcard4android = "03ef99a"
commons-collections = "4.4"
commons-lang = "3.14.0"
Expand Down