@@ -10,7 +10,6 @@ import android.Manifest
1010import android.accounts.Account
1111import android.content.ContentProviderClient
1212import android.content.Entity
13- import android.os.Build
1413import android.provider.CalendarContract
1514import android.provider.CalendarContract.Calendars
1615import android.provider.CalendarContract.Events
@@ -20,41 +19,61 @@ import androidx.test.rule.GrantPermissionRule
2019import at.bitfire.synctools.storage.calendar.AndroidCalendar
2120import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider
2221import org.junit.Assert
23- import org.junit.rules.ExternalResource
2422import org.junit.rules.RuleChain
23+ import org.junit.rules.TestRule
24+ import org.junit.runner.Description
25+ import org.junit.runners.model.Statement
2526import 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