Skip to content

Commit 20f1568

Browse files
authored
Merge pull request #3 from JenteJan/fix/strip-rrule-write
Strip leading RRULE: prefix when writing recurrence rule (fixes Invalid recurrence rule)
2 parents 2d9e977 + 0084552 commit 20f1568

File tree

11 files changed

+363
-176
lines changed

11 files changed

+363
-176
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.0.5
4+
5+
### Bug Fixes
6+
- Fixed recurring-event creation validation for RRULE strings (e.g. `RRULE:FREQ=DAILY;COUNT=10`) that previously caused "Invalid recurrence rule" errors when calling `createEvent`. Thanks to @jaydaptif-solutions for reporting and testing the fix.
7+
38
## 1.0.4
49
### Bug Fixes
510
- Fixed timezone handling issue by updating the `timezone` dependency to version `0.10.1`, ensuring better compatibility and functionality across different platforms.

android/src/main/kotlin/com/ahmtydn/calendar_bridge/EventManager.kt

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import android.database.Cursor
77
import android.provider.CalendarContract
88
import kotlinx.coroutines.Dispatchers
99
import kotlinx.coroutines.withContext
10-
import java.text.SimpleDateFormat
1110
import java.util.*
1211

1312
class EventManager(private val context: Context) {
@@ -38,12 +37,10 @@ class EventManager(private val context: Context) {
3837
var selectionArgs: Array<String>
3938

4039
if (eventIds != null && eventIds.isNotEmpty()) {
41-
// Query specific events by ID
4240
val placeholders = eventIds.joinToString(",") { "?" }
4341
selection = "${CalendarContract.Instances.EVENT_ID} IN ($placeholders)"
4442
selectionArgs = eventIds.toTypedArray()
4543
} else {
46-
// Query by calendar and date range using Instances
4744
val selectionParts = mutableListOf("${CalendarContract.Events.CALENDAR_ID} = ?")
4845
val argsList = mutableListOf(calendarId)
4946

@@ -61,7 +58,6 @@ class EventManager(private val context: Context) {
6158
selectionArgs = argsList.toTypedArray()
6259
}
6360

64-
6561
val instancesUri = CalendarContract.Instances.CONTENT_URI.buildUpon().apply {
6662
ContentUris.appendId(this, startDate ?: 0L)
6763
ContentUris.appendId(this, endDate ?: Long.MAX_VALUE)
@@ -103,19 +99,19 @@ class EventManager(private val context: Context) {
10399

104100
description?.let { eventMap["description"] = it }
105101
location?.let { eventMap["location"] = it }
106-
rrule?.let { eventMap["recurrenceRule"] = it }
102+
rrule?.let {
103+
eventMap["recurrenceRule"] = "RRULE:$it"
104+
}
107105

108106
if (rrule != null) {
109107
eventMap["originalStart"] = originalStartTime
110108
}
111109

112-
// Get attendees
113110
val attendees = getEventAttendees(eventId)
114111
if (attendees.isNotEmpty()) {
115112
eventMap["attendees"] = attendees
116113
}
117114

118-
// Get reminders
119115
val reminders = getEventReminders(eventId)
120116
if (reminders.isNotEmpty()) {
121117
eventMap["reminders"] = reminders
@@ -169,8 +165,14 @@ class EventManager(private val context: Context) {
169165
put(CalendarContract.Events.STATUS, statusFromString(it as String))
170166
}
171167

172-
arguments["recurrenceRule"]?.let {
173-
put(CalendarContract.Events.RRULE, it as String)
168+
arguments["recurrenceRule"]?.let {
169+
val raw = it as String
170+
val normalized = if (raw.startsWith("RRULE:", ignoreCase = true)) {
171+
raw.substring(6).trim()
172+
} else {
173+
raw.trim()
174+
}
175+
put(CalendarContract.Events.RRULE, normalized)
174176
}
175177
}
176178

@@ -180,12 +182,10 @@ class EventManager(private val context: Context) {
180182
val eventId = uri.lastPathSegment
181183
?: throw CalendarException.PlatformError("Failed to get event ID")
182184

183-
// Add attendees if provided
184185
arguments["attendees"]?.let { attendees ->
185186
addEventAttendees(eventId, attendees as List<Map<String, Any>>)
186187
}
187188

188-
// Add reminders if provided
189189
arguments["reminders"]?.let { reminders ->
190190
addEventReminders(eventId, reminders as List<Map<String, Any>>)
191191
}
@@ -197,7 +197,6 @@ class EventManager(private val context: Context) {
197197
val eventId = arguments["eventId"] as? String
198198
?: throw CalendarException.InvalidArgument("Event ID is required")
199199

200-
// Check if event exists
201200
val cursor = context.contentResolver.query(
202201
CalendarContract.Events.CONTENT_URI,
203202
arrayOf(CalendarContract.Events._ID),
@@ -246,8 +245,14 @@ class EventManager(private val context: Context) {
246245
values.put(CalendarContract.Events.STATUS, statusFromString(it as String))
247246
}
248247

249-
arguments["recurrenceRule"]?.let {
250-
values.put(CalendarContract.Events.RRULE, it as String)
248+
arguments["recurrenceRule"]?.let {
249+
val raw = it as String
250+
val normalized = if (raw.startsWith("RRULE:", ignoreCase = true)) {
251+
raw.substring(6).trim()
252+
} else {
253+
raw.trim()
254+
}
255+
values.put(CalendarContract.Events.RRULE, normalized)
251256
}
252257

253258
val updatedRows = context.contentResolver.update(
@@ -261,35 +266,28 @@ class EventManager(private val context: Context) {
261266
throw CalendarException.PlatformError("Failed to update event")
262267
}
263268

264-
// Update attendees if provided
265269
arguments["attendees"]?.let { attendees ->
266-
// Delete existing attendees
267270
context.contentResolver.delete(
268271
CalendarContract.Attendees.CONTENT_URI,
269272
"${CalendarContract.Attendees.EVENT_ID} = ?",
270273
arrayOf(eventId)
271274
)
272-
// Add new attendees
273275
addEventAttendees(eventId, attendees as List<Map<String, Any>>)
274276
}
275277

276-
// Update reminders if provided
277278
arguments["reminders"]?.let { reminders ->
278-
// Delete existing reminders
279279
context.contentResolver.delete(
280280
CalendarContract.Reminders.CONTENT_URI,
281281
"${CalendarContract.Reminders.EVENT_ID} = ?",
282282
arrayOf(eventId)
283283
)
284-
// Add new reminders
285284
addEventReminders(eventId, reminders as List<Map<String, Any>>)
286285
}
287286

288287
return@withContext eventId
289288
}
290289

291290
suspend fun deleteEvent(calendarId: String, eventId: String): Boolean = withContext(Dispatchers.IO) {
292-
// Verify event exists in the specified calendar
293291
val cursor = context.contentResolver.query(
294292
CalendarContract.Events.CONTENT_URI,
295293
arrayOf(CalendarContract.Events._ID, CalendarContract.Events.CALENDAR_ID),
@@ -314,7 +312,6 @@ class EventManager(private val context: Context) {
314312
}
315313

316314
suspend fun deleteEventInstance(calendarId: String, eventId: String, startDate: Long, followingInstances: Boolean): Boolean = withContext(Dispatchers.IO) {
317-
// Verify event exists in the specified calendar
318315
val cursor = context.contentResolver.query(
319316
CalendarContract.Events.CONTENT_URI,
320317
arrayOf(CalendarContract.Events._ID, CalendarContract.Events.CALENDAR_ID, CalendarContract.Events.RRULE),
@@ -332,7 +329,6 @@ class EventManager(private val context: Context) {
332329
isRecurring = !rrule.isNullOrEmpty()
333330
} ?: throw CalendarException.EventNotFound(eventId)
334331

335-
// For recurring events, create an exception instead of deleting
336332
if (isRecurring) {
337333
val exceptionValues = ContentValues().apply {
338334
put(CalendarContract.Events.ORIGINAL_ID, eventId)
@@ -341,16 +337,13 @@ class EventManager(private val context: Context) {
341337
put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CANCELED)
342338

343339
if (followingInstances) {
344-
// For future events, we need to modify the original event's RRULE
345-
// This is a simplified implementation - in practice, you'd need to update the RRULE
346340
put(CalendarContract.Events.DTSTART, startDate)
347341
}
348342
}
349343

350344
val uri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, exceptionValues)
351345
return@withContext uri != null
352346
} else {
353-
// For non-recurring events, just delete normally
354347
return@withContext deleteEvent(calendarId, eventId)
355348
}
356349
}

0 commit comments

Comments
 (0)