Skip to content

Commit ad80101

Browse files
committed
Composition over inheritance: extract EventAndExceptions support into separate class (decorator)
1 parent d4959bd commit ad80101

File tree

5 files changed

+168
-67
lines changed

5 files changed

+168
-67
lines changed

lib/src/androidTest/kotlin/at/bitfire/synctools/storage/calendar/AndroidCalendarTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ class AndroidCalendarTest {
296296
Events.TITLE to "Some Event 1"
297297
)))
298298

299-
calendar.deleteEventAndExceptions(id)
299+
calendar.deleteEvent(id)
300300

301301
assertNull(calendar.getEvent(id))
302302
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.storage.calendar
8+
9+
import android.Manifest
10+
import android.accounts.Account
11+
import android.content.ContentProviderClient
12+
import android.provider.CalendarContract
13+
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
14+
import androidx.test.platform.app.InstrumentationRegistry
15+
import androidx.test.rule.GrantPermissionRule
16+
import at.bitfire.ical4android.util.MiscUtils.closeCompat
17+
import org.junit.After
18+
import org.junit.Before
19+
import org.junit.Rule
20+
import org.junit.Test
21+
22+
class AndroidRecurringCalendarTest {
23+
24+
@get:Rule
25+
val permissonRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)
26+
27+
private val testAccount = Account(javaClass.name, ACCOUNT_TYPE_LOCAL)
28+
29+
lateinit var client: ContentProviderClient
30+
lateinit var provider: AndroidCalendarProvider
31+
32+
@Before
33+
fun setUp() {
34+
val context = InstrumentationRegistry.getInstrumentation().targetContext
35+
client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
36+
provider = AndroidCalendarProvider(testAccount, client)
37+
}
38+
39+
@After
40+
fun tearDown() {
41+
client.closeCompat()
42+
}
43+
44+
45+
@Test
46+
fun testAddEventAndExceptions() {
47+
// TODO
48+
}
49+
50+
@Test
51+
fun testUpdateEventAndExceptions() {
52+
// TODO
53+
}
54+
55+
@Test
56+
fun testDeleteEventAndExceptions() {
57+
// TODO
58+
}
59+
60+
}

lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/AndroidCalendar.kt

Lines changed: 9 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -95,27 +95,7 @@ class AndroidCalendar(
9595
}
9696
}
9797

98-
/**
99-
* Adds an event and all its exceptions.
100-
*/
101-
fun addEventAndExceptions(eventAndExceptions: EventAndExceptions) {
102-
try {
103-
val batch = CalendarBatchOperation(client)
104-
addEvent(eventAndExceptions.main, batch)
105-
106-
/* Add exceptions. We don't have to set ORIGINAL_ID of each exception to the ID of
107-
the main event because the content provider associates events with their exceptions
108-
using _SYNC_ID / ORIGINAL_SYNC_ID. */
109-
for (exception in eventAndExceptions.exceptions)
110-
addEvent(exception, batch)
111-
112-
batch.commit()
113-
} catch (e: RemoteException) {
114-
throw LocalStorageException("Couldn't insert event/exceptions", e)
115-
}
116-
}
117-
118-
private fun addEvent(entity: Entity, batch: CalendarBatchOperation) {
98+
fun addEvent(entity: Entity, batch: CalendarBatchOperation) {
11999
// insert main row
120100
batch += CpoBuilder.newInsert(eventsUri)
121101
.withValues(entity.entityValues)
@@ -290,7 +270,7 @@ class AndroidCalendar(
290270
try {
291271
val rebuild = eventUpdateNeedsRebuild(id, entity.entityValues) ?: true
292272
if (rebuild) {
293-
deleteEventAndExceptions(id)
273+
deleteEvent(id)
294274
return addEvent(entity)
295275
}
296276

@@ -305,7 +285,7 @@ class AndroidCalendar(
305285
}
306286
}
307287

308-
private fun updateEvent(id: Long, entity: Entity, batch: CalendarBatchOperation) {
288+
fun updateEvent(id: Long, entity: Entity, batch: CalendarBatchOperation) {
309289
deleteDataRows(id, batch)
310290

311291
// update main row
@@ -322,33 +302,6 @@ class AndroidCalendar(
322302
})
323303
}
324304

325-
/**
326-
* Adds an event and all its exceptions.
327-
*/
328-
fun updateEventAndExceptions(id: Long, eventAndExceptions: EventAndExceptions) {
329-
try {
330-
val rebuild = eventUpdateNeedsRebuild(id, eventAndExceptions.main.entityValues) ?: true
331-
if (rebuild) {
332-
deleteEventAndExceptions(id)
333-
addEventAndExceptions(eventAndExceptions)
334-
return
335-
}
336-
337-
// update main event
338-
val batch = CalendarBatchOperation(client)
339-
updateEvent(id, eventAndExceptions.main, batch)
340-
341-
// remove and add exceptions again
342-
batch += CpoBuilder.newDelete(eventsUri).withSelection("${Events.ORIGINAL_ID}=?", arrayOf(id.toString()))
343-
for (exception in eventAndExceptions.exceptions)
344-
addEvent(exception, batch)
345-
346-
batch.commit()
347-
} catch (e: RemoteException) {
348-
throw LocalStorageException("Couldn't update event/exceptions", e)
349-
}
350-
}
351-
352305
/**
353306
* Deletes data rows from events, but only those with a known CONTENT_URI that we are also able to
354307
* build. This should prevent accidental deletion of unknown data rows like they may be used by calendar
@@ -386,7 +339,7 @@ class AndroidCalendar(
386339
*
387340
* @return whether the event can't be updated/needs to be re-created; or `null` if existing values couldn't be determined
388341
*/
389-
private fun eventUpdateNeedsRebuild(id: Long, newValues: ContentValues): Boolean? {
342+
internal fun eventUpdateNeedsRebuild(id: Long, newValues: ContentValues): Boolean? {
390343
val existingValues = getEventRow(id, arrayOf(Events.STATUS)) ?: return null
391344
return existingValues.getAsInteger(Events.STATUS) != null && newValues.getAsInteger(Events.STATUS) == null
392345
}
@@ -410,23 +363,15 @@ class AndroidCalendar(
410363
}
411364

412365
/**
413-
* Deletes an event and all its potential exceptions.
366+
* Deletes an event row.
367+
*
368+
* The content provider automatically deletes associated data rows, but doesn't touch exceptions.
414369
*
415370
* @param id ID of the event
416371
*/
417-
fun deleteEventAndExceptions(id: Long) {
372+
fun deleteEvent(id: Long) {
418373
try {
419-
val batch = CalendarBatchOperation(client)
420-
421-
// delete main event
422-
batch += CpoBuilder.newDelete(eventUri(id))
423-
424-
// delete exceptions, too (not automatically done by provider)
425-
batch += CpoBuilder
426-
.newDelete(eventsUri)
427-
.withSelection("${Events.ORIGINAL_ID}=?", arrayOf(id.toString()))
428-
429-
batch.commit()
374+
client.delete(eventUri(id), null, null)
430375
} catch (e: RemoteException) {
431376
throw LocalStorageException("Couldn't delete event $id", e)
432377
}

lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/AndroidEvent2.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,14 @@ class AndroidEvent2(
9797

9898
// shortcuts to upper level
9999

100+
/** See [AndroidCalendar.updateEventRow] */
100101
fun update(values: ContentValues) = calendar.updateEventRow(id, values)
102+
103+
/** See [AndroidCalendar.updateEvent] */
101104
fun update(entity: Entity) = calendar.updateEvent(id, entity)
102-
fun update(eventAndExceptions: EventAndExceptions) = calendar.updateEventAndExceptions(id, eventAndExceptions)
103-
fun deleteWithExceptions() = calendar.deleteEventAndExceptions(id)
105+
106+
/** See [AndroidCalendar.deleteEvent] */
107+
fun delete() = calendar.deleteEvent(id)
104108

105109

106110
// helpers
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.synctools.storage.calendar
8+
9+
import android.os.RemoteException
10+
import android.provider.CalendarContract.Events
11+
import at.bitfire.synctools.storage.BatchOperation.CpoBuilder
12+
import at.bitfire.synctools.storage.LocalStorageException
13+
14+
/**
15+
* Decorator for [AndroidCalendar] that adds support for [EventAndExceptions]
16+
* data objects.
17+
*/
18+
class AndroidRecurringCalendar(
19+
private val calendar: AndroidCalendar
20+
) {
21+
22+
/**
23+
* Adds an event and all its exceptions.
24+
*/
25+
fun addEventAndExceptions(eventAndExceptions: EventAndExceptions) {
26+
try {
27+
val batch = CalendarBatchOperation(calendar.client)
28+
calendar.addEvent(eventAndExceptions.main, batch)
29+
30+
/* Add exceptions. We don't have to set ORIGINAL_ID of each exception to the ID of
31+
the main event because the content provider associates events with their exceptions
32+
using _SYNC_ID / ORIGINAL_SYNC_ID. */
33+
for (exception in eventAndExceptions.exceptions)
34+
calendar.addEvent(exception, batch)
35+
36+
batch.commit()
37+
} catch (e: RemoteException) {
38+
throw LocalStorageException("Couldn't insert event/exceptions", e)
39+
}
40+
}
41+
42+
/**
43+
* Adds an event and all its exceptions.
44+
*/
45+
fun updateEventAndExceptions(id: Long, eventAndExceptions: EventAndExceptions) {
46+
try {
47+
val rebuild = calendar.eventUpdateNeedsRebuild(id, eventAndExceptions.main.entityValues) ?: true
48+
if (rebuild) {
49+
deleteEventAndExceptions(id)
50+
addEventAndExceptions(eventAndExceptions)
51+
return
52+
}
53+
54+
// update main event
55+
val batch = CalendarBatchOperation(calendar.client)
56+
calendar.updateEvent(id, eventAndExceptions.main, batch)
57+
58+
// remove and add exceptions again
59+
batch += CpoBuilder.newDelete(calendar.eventsUri).withSelection("${Events.ORIGINAL_ID}=?", arrayOf(id.toString()))
60+
for (exception in eventAndExceptions.exceptions)
61+
calendar.addEvent(exception, batch)
62+
63+
batch.commit()
64+
} catch (e: RemoteException) {
65+
throw LocalStorageException("Couldn't update event/exceptions", e)
66+
}
67+
}
68+
69+
/**
70+
* Deletes an event and all its potential exceptions.
71+
*
72+
* @param id ID of the event
73+
*/
74+
fun deleteEventAndExceptions(id: Long) {
75+
try {
76+
val batch = CalendarBatchOperation(calendar.client)
77+
78+
// delete main event
79+
batch += CpoBuilder.newDelete(calendar.eventUri(id))
80+
81+
// delete exceptions, too (not automatically done by provider)
82+
batch += CpoBuilder
83+
.newDelete(calendar.eventsUri)
84+
.withSelection("${Events.ORIGINAL_ID}=?", arrayOf(id.toString()))
85+
86+
batch.commit()
87+
} catch (e: RemoteException) {
88+
throw LocalStorageException("Couldn't delete event $id", e)
89+
}
90+
}
91+
92+
}

0 commit comments

Comments
 (0)