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:
@@ -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())
|
// Deterministic UID = the reconciliation key. A re-sync reads it back
|
||||||
val values = ContentValues().apply {
|
// (queryManagedEvents) and diffs on it, so a contact never doubles.
|
||||||
put(
|
val values = buildEventInsertValues(form, uid, form.toWriteTimes(ZoneId.systemDefault()))
|
||||||
CalendarContract.Events.CALENDAR_ID,
|
val eventId = resolver.insert(CalendarContract.Events.CONTENT_URI, values.toContentValues())
|
||||||
requireNotNull(form.calendarId) { "EventForm.calendarId is required" },
|
|
||||||
)
|
|
||||||
// Deterministic UID = the reconciliation key. A re-sync reads it back
|
|
||||||
// (queryManagedEvents) and diffs on it, so a contact never doubles.
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
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())
|
// A globally-unique UID so a later .ics backup/restore can identify
|
||||||
val values = ContentValues().apply {
|
// the event and not duplicate it on re-import (the provider leaves
|
||||||
put(
|
// this null for events it didn't sync). Older rows without one fall
|
||||||
CalendarContract.Events.CALENDAR_ID,
|
// back to a stable synthesised UID at export time (deriveIcsUid).
|
||||||
requireNotNull(form.calendarId) { "EventForm.calendarId is required" },
|
val values = buildEventInsertValues(
|
||||||
)
|
form,
|
||||||
// A globally-unique UID so a later .ics backup/restore can identify
|
uid = "${UUID.randomUUID()}@calendula",
|
||||||
// the event and not duplicate it on re-import (the provider leaves
|
times = form.toWriteTimes(ZoneId.systemDefault()),
|
||||||
// this null for events it didn't sync). Older rows without one fall
|
).toContentValues().apply {
|
||||||
// back to a stable synthesised UID at export time (deriveIcsUid).
|
|
||||||
put(CalendarContract.Events.UID_2445, "${UUID.randomUUID()}@calendula")
|
|
||||||
put(CalendarContract.Events.TITLE, form.title.trim())
|
|
||||||
put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0)
|
|
||||||
put(CalendarContract.Events.DTSTART, times.dtStartMillis)
|
|
||||||
// The provider's invariant: recurring rows carry RRULE+DURATION
|
|
||||||
// (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,19 +767,7 @@ 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.
|
|
||||||
encodedReminders(form, allDayReminderTimeMinutes)
|
|
||||||
.forEach { minutes ->
|
|
||||||
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 event $eventId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Guests are best-effort like reminders: a row that fails to attach is
|
// Guests are best-effort like reminders: a row that fails to attach is
|
||||||
// logged, not surfaced as a failed create. Calendula never sends an
|
// logged, not surfaced as a failed create. Calendula never sends an
|
||||||
// invitation — it only writes the rows; the backend decides delivery.
|
// invitation — it only writes the rows; the backend decides delivery.
|
||||||
@@ -839,6 +775,24 @@ class AndroidCalendarDataSource @Inject constructor(
|
|||||||
return eventId
|
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 {
|
||||||
|
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 event $eventId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun updateEvent(
|
override fun updateEvent(
|
||||||
eventId: Long,
|
eventId: Long,
|
||||||
original: EventForm,
|
original: EventForm,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user