refactor(calendar): share the insert values builder + reminder seeding

insertManagedEvent duplicated ~45 lines of insertEvent (ContentValues build
and the reminder-seeding loop). Both now go through buildEventInsertValues
in EventWriteMapper and a shared seedReminders, differing only in the UID
they stamp and the colour/attendee columns insertEvent layers on top.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 22:40:26 +02:00
parent 4e125e58d5
commit 8e3a17fdc3
2 changed files with 69 additions and 79 deletions

View File

@@ -416,47 +416,15 @@ class AndroidCalendarDataSource @Inject constructor(
} ?: emptyList() } ?: emptyList()
override fun insertManagedEvent(form: EventForm, uid: String, allDayReminderTimeMinutes: Int): Long { override fun insertManagedEvent(form: EventForm, uid: String, allDayReminderTimeMinutes: Int): Long {
val times = form.toWriteTimes(ZoneId.systemDefault())
val values = ContentValues().apply {
put(
CalendarContract.Events.CALENDAR_ID,
requireNotNull(form.calendarId) { "EventForm.calendarId is required" },
)
// Deterministic UID = the reconciliation key. A re-sync reads it back // Deterministic UID = the reconciliation key. A re-sync reads it back
// (queryManagedEvents) and diffs on it, so a contact never doubles. // (queryManagedEvents) and diffs on it, so a contact never doubles.
put(CalendarContract.Events.UID_2445, uid) val values = buildEventInsertValues(form, uid, form.toWriteTimes(ZoneId.systemDefault()))
put(CalendarContract.Events.TITLE, form.title.trim()) val eventId = resolver.insert(CalendarContract.Events.CONTENT_URI, values.toContentValues())
put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0)
put(CalendarContract.Events.DTSTART, times.dtStartMillis)
if (form.rrule == null) {
put(CalendarContract.Events.DTEND, times.dtEndMillis)
} else {
put(CalendarContract.Events.RRULE, form.rrule)
put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay))
}
put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone)
put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue())
put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue())
form.location.trim().takeIf { it.isNotEmpty() }
?.let { put(CalendarContract.Events.EVENT_LOCATION, it) }
form.description.trim().takeIf { it.isNotEmpty() }
?.let { put(CalendarContract.Events.DESCRIPTION, it) }
}
val eventId = resolver.insert(CalendarContract.Events.CONTENT_URI, values)
?.let(ContentUris::parseId) ?.let(ContentUris::parseId)
?: throw WriteFailedException("insert managed event into calendar id=${form.calendarId}") ?: throw WriteFailedException("insert managed event into calendar id=${form.calendarId}")
// Seeded once, then never re-touched by sync — so a user who moves a // Seeded once, then never re-touched by sync — so a user who moves a
// birthday reminder keeps their change. // birthday reminder keeps their change.
encodedReminders(form, allDayReminderTimeMinutes).forEach { minutes -> seedReminders(eventId, form, allDayReminderTimeMinutes)
val reminder = ContentValues().apply {
put(CalendarContract.Reminders.EVENT_ID, eventId)
put(CalendarContract.Reminders.MINUTES, minutes)
put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
}
if (resolver.insert(CalendarContract.Reminders.CONTENT_URI, reminder) == null) {
Log.w(TAG, "Failed to attach reminder ($minutes min) to managed event $eventId")
}
}
return eventId return eventId
} }
@@ -779,35 +747,15 @@ class AndroidCalendarDataSource @Inject constructor(
.distinct() .distinct()
override fun insertEvent(form: EventForm, allDayReminderTimeMinutes: Int): Long { override fun insertEvent(form: EventForm, allDayReminderTimeMinutes: Int): Long {
val times = form.toWriteTimes(ZoneId.systemDefault())
val values = ContentValues().apply {
put(
CalendarContract.Events.CALENDAR_ID,
requireNotNull(form.calendarId) { "EventForm.calendarId is required" },
)
// A globally-unique UID so a later .ics backup/restore can identify // A globally-unique UID so a later .ics backup/restore can identify
// the event and not duplicate it on re-import (the provider leaves // the event and not duplicate it on re-import (the provider leaves
// this null for events it didn't sync). Older rows without one fall // this null for events it didn't sync). Older rows without one fall
// back to a stable synthesised UID at export time (deriveIcsUid). // back to a stable synthesised UID at export time (deriveIcsUid).
put(CalendarContract.Events.UID_2445, "${UUID.randomUUID()}@calendula") val values = buildEventInsertValues(
put(CalendarContract.Events.TITLE, form.title.trim()) form,
put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0) uid = "${UUID.randomUUID()}@calendula",
put(CalendarContract.Events.DTSTART, times.dtStartMillis) times = form.toWriteTimes(ZoneId.systemDefault()),
// The provider's invariant: recurring rows carry RRULE+DURATION ).toContentValues().apply {
// (and no DTEND), one-off rows carry DTEND.
if (form.rrule == null) {
put(CalendarContract.Events.DTEND, times.dtEndMillis)
} else {
put(CalendarContract.Events.RRULE, form.rrule)
put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay))
}
put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone)
put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue())
put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue())
form.location.trim().takeIf { it.isNotEmpty() }
?.let { put(CalendarContract.Events.EVENT_LOCATION, it) }
form.description.trim().takeIf { it.isNotEmpty() }
?.let { put(CalendarContract.Events.DESCRIPTION, it) }
// A null colour just leaves both columns unset (the event inherits // A null colour just leaves both columns unset (the event inherits
// its calendar's colour), so only the key/raw cases are written. // its calendar's colour), so only the key/raw cases are written.
when { when {
@@ -819,10 +767,21 @@ class AndroidCalendarDataSource @Inject constructor(
val uri = resolver.insert(CalendarContract.Events.CONTENT_URI, values) val uri = resolver.insert(CalendarContract.Events.CONTENT_URI, values)
?: throw WriteFailedException("insert event into calendar id=${form.calendarId}") ?: throw WriteFailedException("insert event into calendar id=${form.calendarId}")
val eventId = ContentUris.parseId(uri) val eventId = ContentUris.parseId(uri)
// Best effort (spec §8): the event exists at this point — a reminder seedReminders(eventId, form, allDayReminderTimeMinutes)
// that fails to attach is logged, not surfaced as a failed create. // Guests are best-effort like reminders: a row that fails to attach is
encodedReminders(form, allDayReminderTimeMinutes) // logged, not surfaced as a failed create. Calendula never sends an
.forEach { minutes -> // invitation — it only writes the rows; the backend decides delivery.
insertAttendees(eventId, form.attendees)
return eventId
}
/**
* Attach [form]'s reminders to a freshly inserted event. Best effort
* (spec §8): the event exists at this point — a reminder that fails to
* attach is logged, not surfaced as a failed create.
*/
private fun seedReminders(eventId: Long, form: EventForm, allDayReminderTimeMinutes: Int) {
encodedReminders(form, allDayReminderTimeMinutes).forEach { minutes ->
val reminder = ContentValues().apply { val reminder = ContentValues().apply {
put(CalendarContract.Reminders.EVENT_ID, eventId) put(CalendarContract.Reminders.EVENT_ID, eventId)
put(CalendarContract.Reminders.MINUTES, minutes) put(CalendarContract.Reminders.MINUTES, minutes)
@@ -832,11 +791,6 @@ class AndroidCalendarDataSource @Inject constructor(
Log.w(TAG, "Failed to attach reminder ($minutes min) to event $eventId") Log.w(TAG, "Failed to attach reminder ($minutes min) to event $eventId")
} }
} }
// Guests are best-effort like reminders: a row that fails to attach is
// logged, not surfaced as a failed create. Calendula never sends an
// invitation — it only writes the rows; the backend decides delivery.
insertAttendees(eventId, form.attendees)
return eventId
} }
override fun updateEvent( override fun updateEvent(

View File

@@ -49,6 +49,42 @@ internal fun EventWriteTimes.toRfc2445Duration(isAllDay: Boolean): String = if (
"P${(dtEndMillis - dtStartMillis) / 1_000L}S" "P${(dtEndMillis - dtStartMillis) / 1_000L}S"
} }
/**
* Column values for a brand-new Events row: identity, times (the provider's
* invariant — recurring rows carry RRULE+DURATION and no DTEND, one-off rows
* carry DTEND), availability/access level and trimmed optional text. Shared by
* the user-event and managed-event insert paths, which differ only in the
* [uid] they stamp; colour and attendees are user-event concerns the caller
* layers on top.
*/
internal fun buildEventInsertValues(
form: EventForm,
uid: String,
times: EventWriteTimes,
): Map<String, Any?> = buildMap {
put(
CalendarContract.Events.CALENDAR_ID,
requireNotNull(form.calendarId) { "EventForm.calendarId is required" },
)
put(CalendarContract.Events.UID_2445, uid)
put(CalendarContract.Events.TITLE, form.title.trim())
put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0)
put(CalendarContract.Events.DTSTART, times.dtStartMillis)
if (form.rrule == null) {
put(CalendarContract.Events.DTEND, times.dtEndMillis)
} else {
put(CalendarContract.Events.RRULE, form.rrule)
put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay))
}
put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone)
put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue())
put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue())
form.location.trim().takeIf { it.isNotEmpty() }
?.let { put(CalendarContract.Events.EVENT_LOCATION, it) }
form.description.trim().takeIf { it.isNotEmpty() }
?.let { put(CalendarContract.Events.DESCRIPTION, it) }
}
/** /**
* Dirty-checked column values for updating an existing Events row: only what * Dirty-checked column values for updating an existing Events row: only what
* the user actually changed is written, so untouched fields can't stomp * the user actually changed is written, so untouched fields can't stomp