Skip to content

Commit 86742f5

Browse files
sunkuprfc2822
andauthored
Don't upload event when calendar is read only (#587)
* Make readOnly a LocalCollection property * Move readOnly detection to SyncManager * Add readOnly state access to LocalCalendar * Add not implemented error to readOnly state access of LocalJtxCollection * Handle read-only state of calendar at dirty events upload * Handle read-only state of calendar at processing of locally deleted events * Remove todo and update kdoc * Fix indenting * Add read-only prop to LocalTestCollection * Add read-only state access to LocalTaskList * LocalTestCollection: don't set read-only * Update ical4android (for new KDoc) * Make LocalCollection readOnly-API read only and take value from content provider during populate() * SyncManager: use readOnly direct from localCollection * Lift resetDeleted up to LocalResource * Override and use resetDeleted for LocalEvent * Add resetDeleted to LocalJtxICalObject * Add resetDeleted to LocalTask * Add resetDeleted to LocalTask * Add resetDeleted to LocalTestResource * Provide default access level --------- Co-authored-by: Ricki Hirner <[email protected]>
1 parent df2b7d2 commit 86742f5

File tree

14 files changed

+123
-33
lines changed

14 files changed

+123
-33
lines changed

app/src/androidTestOse/kotlin/at/bitfire/davdroid/syncadapter/LocalTestCollection.kt

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class LocalTestCollection: LocalCollection<LocalTestResource> {
1616

1717
val entries = mutableListOf<LocalTestResource>()
1818

19+
override val readOnly: Boolean
20+
get() = throw NotImplementedError()
21+
1922
override fun findDeleted() = entries.filter { it.deleted }
2023
override fun findDirty() = entries.filter { it.dirty }
2124

app/src/androidTestOse/kotlin/at/bitfire/davdroid/syncadapter/LocalTestResource.kt

+1
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ class LocalTestResource: LocalResource<Any> {
3434
override fun add() = throw NotImplementedError()
3535
override fun update(data: Any) = throw NotImplementedError()
3636
override fun delete() = throw NotImplementedError()
37+
override fun resetDeleted() = throw NotImplementedError()
3738

3839
}

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddress.kt

+1-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,4 @@ package at.bitfire.davdroid.resource
66

77
import at.bitfire.vcard4android.Contact
88

9-
interface LocalAddress: LocalResource<Contact> {
10-
11-
fun resetDeleted()
12-
13-
}
9+
interface LocalAddress: LocalResource<Contact>

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt

+9
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ class LocalCalendar private constructor(
9191
override val title: String
9292
get() = displayName ?: id.toString()
9393

94+
private var accessLevel: Int = Calendars.CAL_ACCESS_OWNER // assume full access if not specified
95+
override val readOnly
96+
get() = accessLevel <= Calendars.CAL_ACCESS_READ
97+
9498
override var lastSyncState: SyncState?
9599
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor ->
96100
if (cursor.moveToNext())
@@ -105,6 +109,11 @@ class LocalCalendar private constructor(
105109
}
106110

107111

112+
override fun populate(info: ContentValues) {
113+
super.populate(info)
114+
accessLevel = info.getAsInteger(Calendars.CALENDAR_ACCESS_LEVEL) ?: Calendars.CAL_ACCESS_OWNER
115+
}
116+
108117
fun update(info: Collection, updateColor: Boolean) =
109118
update(valuesFromCollectionInfo(info, updateColor))
110119

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ interface LocalCollection<out T: LocalResource<*>> {
1717

1818
var lastSyncState: SyncState?
1919

20+
/**
21+
* Whether the collection should be treated as read-only on sync.
22+
* Stops uploading dirty events (Server side changes are still downloaded).
23+
*/
24+
val readOnly: Boolean
25+
2026
/**
2127
* Finds local resources of this collection which have been marked as *deleted* by the user
2228
* or an app acting on their behalf.

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalEvent.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import at.bitfire.ical4android.Ical4Android
2121
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
2222
import net.fortuna.ical4j.model.property.ProdId
2323
import java.util.UUID
24-
2524
class LocalEvent: AndroidEvent, LocalResource<Event> {
2625

2726
companion object {
@@ -51,6 +50,7 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
5150
)
5251
}
5352

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

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

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

265269
}
270+

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Lo
5050
}
5151
}
5252

53+
override val readOnly: Boolean
54+
get() = throw NotImplementedError()
55+
5356
override val tag: String
5457
get() = "jtx-${account.name}-$id"
5558
override val title: String

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxICalObject.kt

+5
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,9 @@ class LocalJtxICalObject(
4747
}
4848

4949
}
50+
51+
override fun resetDeleted() {
52+
throw NotImplementedError()
53+
}
54+
5055
}

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalResource.kt

+5
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,9 @@ interface LocalResource<in TData: Any> {
9090
*/
9191
fun delete(): Int
9292

93+
/**
94+
* Undoes deletion of the data object from the content provider.
95+
*/
96+
fun resetDeleted()
97+
9398
}

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTask.kt

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ class LocalTask: DmfsTask, LocalResource<Task> {
104104
this.flags = flags
105105
}
106106

107+
override fun resetDeleted() {
108+
throw NotImplementedError()
109+
}
110+
107111

108112
object Factory: DmfsTaskFactory<LocalTask> {
109113
override fun fromProvider(taskList: DmfsTaskList<*>, values: ContentValues) =

app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt

+12-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ class LocalTaskList private constructor(
6868

6969
}
7070

71+
private var accessLevel: Int = TaskListColumns.ACCESS_LEVEL_UNDEFINED
72+
override val readOnly
73+
get() =
74+
accessLevel != TaskListColumns.ACCESS_LEVEL_UNDEFINED &&
75+
accessLevel <= TaskListColumns.ACCESS_LEVEL_READ
76+
7177
override val tag: String
7278
get() = "tasks-${account.name}-$id"
7379

@@ -96,8 +102,13 @@ class LocalTaskList private constructor(
96102
}
97103

98104

105+
override fun populate(values: ContentValues) {
106+
super.populate(values)
107+
accessLevel = values.getAsInteger(TaskListColumns.ACCESS_LEVEL)
108+
}
109+
99110
fun update(info: Collection, updateColor: Boolean) =
100-
update(valuesFromCollectionInfo(info, updateColor))
111+
update(valuesFromCollectionInfo(info, updateColor))
101112

102113

103114
override fun findDeleted() = queryTasks(Tasks._DELETED, null)

app/src/main/kotlin/at/bitfire/davdroid/syncadapter/CalendarSyncManager.kt

+65-21
Original file line numberDiff line numberDiff line change
@@ -73,29 +73,74 @@ class CalendarSyncManager(
7373
}
7474

7575
override fun queryCapabilities(): SyncState? =
76-
remoteExceptionContext {
77-
var syncState: SyncState? = null
78-
it.propfind(0, MaxResourceSize.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
79-
if (relation == Response.HrefRelation.SELF) {
80-
response[MaxResourceSize::class.java]?.maxSize?.let { maxSize ->
81-
Logger.log.info("Calendar accepts events up to ${FileUtils.byteCountToDisplaySize(maxSize)}")
82-
}
83-
84-
response[SupportedReportSet::class.java]?.let { supported ->
85-
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
86-
}
87-
syncState = syncState(response)
76+
remoteExceptionContext {
77+
var syncState: SyncState? = null
78+
it.propfind(0, MaxResourceSize.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
79+
if (relation == Response.HrefRelation.SELF) {
80+
response[MaxResourceSize::class.java]?.maxSize?.let { maxSize ->
81+
Logger.log.info("Calendar accepts events up to ${FileUtils.byteCountToDisplaySize(maxSize)}")
82+
}
83+
84+
response[SupportedReportSet::class.java]?.let { supported ->
85+
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
8886
}
87+
syncState = syncState(response)
8988
}
89+
}
9090

91-
Logger.log.info("Calendar supports Collection Sync: $hasCollectionSync")
92-
syncState
91+
Logger.log.info("Calendar supports Collection Sync: $hasCollectionSync")
92+
syncState
93+
}
94+
95+
override fun syncAlgorithm() =
96+
if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
97+
SyncAlgorithm.PROPFIND_REPORT
98+
else
99+
SyncAlgorithm.COLLECTION_SYNC
100+
101+
override fun processLocallyDeleted(): Boolean {
102+
if (localCollection.readOnly) {
103+
var modified = false
104+
for (event in localCollection.findDeleted()) {
105+
Logger.log.warning("Restoring locally deleted event (read-only calendar!)")
106+
localExceptionContext(event) { it.resetDeleted() }
107+
modified = true
93108
}
94109

95-
override fun syncAlgorithm() = if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
96-
SyncAlgorithm.PROPFIND_REPORT
97-
else
98-
SyncAlgorithm.COLLECTION_SYNC
110+
// This is unfortunately ugly: When an event has been inserted to a read-only calendar
111+
// it's not enough to force synchronization (by returning true),
112+
// but we also need to make sure all events are downloaded again.
113+
if (modified)
114+
localCollection.lastSyncState = null
115+
116+
return modified
117+
}
118+
// mirror deletions to remote collection (DELETE)
119+
return super.processLocallyDeleted()
120+
}
121+
122+
override fun uploadDirty(): Boolean {
123+
var modified = false
124+
if (localCollection.readOnly) {
125+
for (event in localCollection.findDirty()) {
126+
Logger.log.warning("Resetting locally modified event to ETag=null (read-only calendar!)")
127+
localExceptionContext(event) { it.clearDirty(null, null) }
128+
modified = true
129+
}
130+
131+
// This is unfortunately ugly: When an event has been inserted to a read-only calendar
132+
// it's not enough to force synchronization (by returning true),
133+
// but we also need to make sure all events are downloaded again.
134+
if (modified)
135+
localCollection.lastSyncState = null
136+
}
137+
138+
// generate UID/file name for newly created events
139+
val superModified = super.uploadDirty()
140+
141+
// return true when any operation returned true
142+
return modified or superModified
143+
}
99144

100145
override fun generateUpload(resource: LocalEvent): RequestBody = localExceptionContext(resource) {
101146
val event = requireNotNull(resource.event)
@@ -143,8 +188,7 @@ class CalendarSyncManager(
143188
}
144189
}
145190

146-
override fun postProcess() {
147-
}
191+
override fun postProcess() {}
148192

149193

150194
// helpers
@@ -195,6 +239,6 @@ class CalendarSyncManager(
195239
}
196240

197241
override fun notifyInvalidResourceTitle(): String =
198-
context.getString(R.string.sync_invalid_event)
242+
context.getString(R.string.sync_invalid_event)
199243

200244
}

app/src/main/kotlin/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt

+2-4
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ class ContactsSyncManager(
105105
infix fun <T> Set<T>.disjunct(other: Set<T>) = (this - other) union (other - this)
106106
}
107107

108-
private val readOnly = localAddressBook.readOnly
109-
110108
private var hasVCard4 = false
111109
private var hasJCard = false
112110
private val groupStrategy = when (accountSettings.getGroupMethod()) {
@@ -177,7 +175,7 @@ class ContactsSyncManager(
177175
SyncAlgorithm.PROPFIND_REPORT
178176

179177
override fun processLocallyDeleted() =
180-
if (readOnly) {
178+
if (localCollection.readOnly) {
181179
var modified = false
182180
for (group in localCollection.findDeletedGroups()) {
183181
Logger.log.warning("Restoring locally deleted group (read-only address book!)")
@@ -205,7 +203,7 @@ class ContactsSyncManager(
205203
override fun uploadDirty(): Boolean {
206204
var modified = false
207205

208-
if (readOnly) {
206+
if (localCollection.readOnly) {
209207
for (group in localCollection.findDirtyGroups()) {
210208
Logger.log.warning("Resetting locally modified group to ETag=null (read-only address book!)")
211209
localExceptionContext(group) { it.clearDirty(null, null) }

gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ androidx-work = "2.9.0"
2828
appIntro = "7.0.0-beta02"
2929
bitfire-cert4android = "f0964cb"
3030
bitfire-dav4jvm = "b30913f"
31-
bitfire-ical4android = "998f6b6"
31+
bitfire-ical4android = "31650d1"
3232
bitfire-vcard4android = "03ef99a"
3333
commons-collections = "4.4"
3434
commons-lang = "3.14.0"

0 commit comments

Comments
 (0)