Skip to content

Commit ee47481

Browse files
committed
Fix InitCalendarProviderRule
1 parent 2724e33 commit ee47481

File tree

5 files changed

+90
-40
lines changed

5 files changed

+90
-40
lines changed

lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package at.bitfire.ical4android.impl
99
import android.accounts.Account
1010
import android.content.ContentProviderClient
1111
import android.provider.CalendarContract.Calendars
12-
import android.provider.CalendarContract.Reminders
1312
import androidx.core.content.contentValuesOf
1413
import at.bitfire.synctools.storage.calendar.AndroidCalendar
1514
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
@@ -30,7 +29,7 @@ object TestCalendar {
3029
?: provider.createAndGetCalendar(contentValuesOf(
3130
Calendars.NAME to UUID.randomUUID().toString(),
3231
Calendars.CALENDAR_DISPLAY_NAME to "ical4android Test Calendar",
33-
Calendars.ALLOWED_REMINDERS to Reminders.METHOD_DEFAULT)
32+
Calendars.CALENDAR_ACCESS_LEVEL to Calendars.CAL_ACCESS_ROOT)
3433
)
3534
}
3635

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ class AndroidCalendarTest {
369369

370370
@Test
371371
fun testNumInstances_RecurringWithExceptions_MatchingOrigInstanceTime() {
372+
// TODO: Fails when
373+
// 1) Force-stop calendar storage
374+
// 2) Clear calendar storage
375+
// 3) Run this this class
376+
372377
val syncId = "recurring-with-exceptions"
373378
val id = calendar.addEvent(Entity(contentValuesOf(
374379
Events.CALENDAR_ID to calendar.id,
@@ -383,15 +388,15 @@ class AndroidCalendarTest {
383388
Events.ORIGINAL_SYNC_ID to syncId,
384389
Events.ORIGINAL_INSTANCE_TIME to 1642640523000 + 2*86400000,
385390
Events.DTSTART to 1642640523000 + 2*86400000 + 3600000, // one hour later
386-
Events.DURATION to "PT1H",
391+
Events.DTEND to 1642640523000 + 2*86400000 + 2*3600000,
387392
Events.TITLE to "Exception on 3rd day",
388393
)))
389394
calendar.addEvent(Entity(contentValuesOf(
390395
Events.CALENDAR_ID to calendar.id,
391396
Events.ORIGINAL_SYNC_ID to syncId,
392397
Events.ORIGINAL_INSTANCE_TIME to 1642640523000 + 4*86400000,
393398
Events.DTSTART to 1642640523000 + 4*86400000 + 3600000, // one hour later
394-
Events.DURATION to "PT1H",
399+
Events.DTEND to 1642640523000 + 4*86400000 + 2*3600000,
395400
Events.TITLE to "Exception on 5th day",
396401
)))
397402
assertEquals(5 - 2, calendar.numInstances(id))

lib/src/main/kotlin/at/bitfire/synctools/storage/BatchOperation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ open class BatchOperation internal constructor(
125125

126126
try {
127127
val ops = toCPO(start, end)
128-
logger.fine("Running ${ops.size} operations ($start .. ${end - 1})")
128+
logger.fine("Running ${ops.size} operation(s) idx $start..${end - 1}")
129129
val partResults = providerClient.applyBatch(ops)
130130

131131
val n = end - start

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,12 @@ class AndroidCalendar(
347347
// themselves and thus belong to the calendar class)
348348

349349
/**
350-
* Finds the amount of direct instances this event has. Exceptions have their own instances
351-
* and are not taken into account by this value.
350+
* Finds the amount of instances this event has. Exceptions generate their own instances and are
351+
* not taken into account by this method.
352352
*
353353
* Use [numInstances] to find the total number of instances (including exceptions) of this event.
354354
*
355-
* @return number of direct event instances (not counting instances of exceptions); *null* if
355+
* @return number of event instances (not counting instances generated by exceptions); *null* if
356356
* the number can't be determined or if the event has no last date (recurring event without last instance)
357357
*
358358
* @throws LocalStorageException when the content provider returns an error
@@ -393,6 +393,13 @@ class AndroidCalendar(
393393
return numInstances
394394
}
395395

396+
fun numInstancesIncludingExceptions(eventId: Long): Int? {
397+
val numDirectInstances = numInstances(eventId) ?: return null
398+
399+
// add instances generated by exceptions
400+
return numDirectInstances
401+
}
402+
396403

397404
// shortcuts to upper level
398405

lib/src/main/kotlin/at/bitfire/synctools/test/InitCalendarProviderRule.kt

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import android.Manifest
1010
import android.accounts.Account
1111
import android.content.ContentProviderClient
1212
import android.content.Entity
13-
import android.os.Build
1413
import android.provider.CalendarContract
1514
import android.provider.CalendarContract.Calendars
1615
import android.provider.CalendarContract.Events
@@ -20,41 +19,61 @@ import androidx.test.rule.GrantPermissionRule
2019
import at.bitfire.synctools.storage.calendar.AndroidCalendar
2120
import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
2221
import org.junit.Assert
23-
import org.junit.rules.ExternalResource
2422
import org.junit.rules.RuleChain
23+
import org.junit.rules.TestRule
24+
import org.junit.runner.Description
25+
import org.junit.runners.model.Statement
2526
import java.util.logging.Logger
2627

2728
/**
28-
* JUnit ClassRule which initializes the AOSP CalendarProvider.
29+
* JUnit ClassRule which initializes the AOSP calendar provider so that queries work as they should.
2930
*
30-
* It seems that the calendar provider unfortunately forgets the very first requests when it is used the very first time,
31-
* maybe by some wrongly synchronized database initialization. So things like querying the instances
32-
* fails in this case.
31+
* It seems that the calendar provider unfortunately forgets the very first requests when it is used the very first time
32+
* (like in a fresh emulator in CI tests or directly after clearing the calendar provider storage), so things like querying
33+
* the instances fails in that case.
3334
*
34-
* So this rule is needed to allow tests which need the calendar provider to succeed even when the calendar provider
35-
* is used the very first time (especially in CI tests / a fresh emulator).
35+
* Android-internal `CalendarProvider2Tests` use a `CalendarProvider2ForTesting` that disables the asynchronous code
36+
* which causes the problems (like `updateTimezoneDependentFields()`). Unfortunately, we can't do that because we have
37+
* to use the real calendar provider.
38+
*
39+
* So this rule brings the calendar provider into its "normal working state" by creating an event and querying
40+
* its instances as long until the result is correct. This works for now, but may fail in the future because
41+
* it's only a trial-and-error workaround.
3642
*/
37-
class InitCalendarProviderRule private constructor() : ExternalResource() {
43+
class InitCalendarProviderRule private constructor() : TestRule {
3844

3945
companion object {
4046

4147
private var isInitialized = false
42-
private val logger = Logger.getLogger(InitCalendarProviderRule::javaClass.name)
4348

4449
fun initialize(): RuleChain = RuleChain
4550
.outerRule(GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))
4651
.around(InitCalendarProviderRule())
4752

4853
}
4954

55+
private val logger
56+
get() = Logger.getLogger(javaClass.name)
57+
5058
val account = Account(javaClass.name, CalendarContract.ACCOUNT_TYPE_LOCAL)
5159

5260

53-
override fun before() {
61+
// TestRule implementation
62+
63+
override fun apply(base: Statement?, description: Description?) = object: Statement() {
64+
override fun evaluate() {
65+
before()
66+
base?.evaluate()
67+
// after() not needed
68+
}
69+
}
70+
71+
72+
// custom wrappers
73+
74+
fun before() {
5475
if (!isInitialized) {
55-
logger.info("Initializing calendar provider")
56-
if (Build.VERSION.SDK_INT < 31)
57-
logger.warning("Calendar provider initialization may or may not work. See InitCalendarProviderRule")
76+
logger.warning("Calendar provider initialization may or may not work. See InitCalendarProviderRule KDoc.")
5877

5978
val context = InstrumentationRegistry.getInstrumentation().targetContext
6079
val client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
@@ -74,28 +93,48 @@ class InitCalendarProviderRule private constructor() : ExternalResource() {
7493
calendarOrNull = createAndVerifyCalendar(provider)
7594
if (calendarOrNull != null)
7695
break
77-
else
78-
Thread.sleep(100)
96+
Thread.sleep(100)
7997
}
8098
val calendar = calendarOrNull ?: throw IllegalStateException("Couldn't create calendar")
8199

82100
try {
83-
// single event init
84-
val normalLocalEventId = calendar.addEvent(Entity(contentValuesOf(
85-
Events.CALENDAR_ID to calendar.id,
86-
Events.DTSTART to 1752075270000,
87-
Events.TITLE to "Event with 1 instance"
88-
)))
89-
calendar.numInstances(normalLocalEventId)
90-
91-
// recurring event init
92-
val recurringEventId = calendar.addEvent(Entity(contentValuesOf(
93-
Events.CALENDAR_ID to calendar.id,
94-
Events.DTSTART to 1752075410000,
95-
Events.TITLE to "Event over 22 years",
96-
Events.RRULE to "FREQ=YEARLY;UNTIL=20740119T010203Z"
97-
)))
98-
calendar.numInstances(recurringEventId)
101+
// insert recurring event and query instances until the result is correct (max 50 times)
102+
val syncId = "test-sync-id"
103+
for (i in 0..50) {
104+
val id = calendar.addEvent(Entity(contentValuesOf(
105+
Events.CALENDAR_ID to calendar.id,
106+
Events._SYNC_ID to syncId,
107+
Events.DTSTART to 1642640523000,
108+
Events.DURATION to "PT1H",
109+
Events.TITLE to "Event with 5 instances, two of them are exceptions",
110+
Events.RRULE to "FREQ=DAILY;COUNT=5"
111+
)))
112+
calendar.addEvent(Entity(contentValuesOf(
113+
Events.CALENDAR_ID to calendar.id,
114+
Events.ORIGINAL_SYNC_ID to syncId,
115+
Events.ORIGINAL_INSTANCE_TIME to 1642640523000 + 2*86400000,
116+
Events.DTSTART to 1642640523000 + 2*86400000 + 3600000, // one hour later
117+
Events.DTEND to 1642640523000 + 2*86400000 + 2*3600000,
118+
Events.TITLE to "Exception on 3rd day",
119+
)))
120+
calendar.addEvent(Entity(contentValuesOf(
121+
Events.CALENDAR_ID to calendar.id,
122+
Events.ORIGINAL_SYNC_ID to syncId,
123+
Events.ORIGINAL_INSTANCE_TIME to 1642640523000 + 4*86400000,
124+
Events.DTSTART to 1642640523000 + 4*86400000 + 3600000, // one hour later
125+
Events.DTEND to 1642640523000 + 4*86400000 + 2*3600000,
126+
Events.TITLE to "Exception on 5th day",
127+
)))
128+
129+
val instances = calendar.numInstances(id)
130+
logger.fine("Recurring event: got $instances instances (should be 3)")
131+
132+
// exit on correct number of instances
133+
if (calendar.getEventRow(id) != null && instances == 3)
134+
break
135+
136+
Thread.sleep(100)
137+
}
99138
} finally {
100139
calendar.delete()
101140
}

0 commit comments

Comments
 (0)