fix(detail): open events whose series starts before 1970 (#34)
DTSTART is stored as UTC epoch millis, so a recurring series anchored before 1970-01-01 (common for yearly birthdays/anniversaries synced over CalDAV) has a legitimately negative DTSTART. The detail and search mappers dropped any row with dtstart < 0, and since the detail query reads the series-master DTSTART (the ancient anchor), every occurrence of such a series became un-openable — surfacing as the generic "Something went wrong." error screen — and the events vanished from search too. Relax the guard to reject only an *absent* DTSTART (isNull), which is the malformed case it was meant to catch; negative epoch millis flow through correctly (Instant/formatting and the all-day reminder decode are all Long-based). Add regression tests for both mappers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,12 +23,16 @@ internal fun ColumnReader.toEventDetailCore(
|
||||
attendees: List<Attendee>,
|
||||
reminders: List<Reminder>,
|
||||
): EventDetail? {
|
||||
val begin = getLong(EventDetailProjection.IDX_DTSTART)
|
||||
|
||||
if (begin < 0L) {
|
||||
Log.w(TAG, "Dropping event with negative dtstart=$begin")
|
||||
// DTSTART is epoch millis in UTC, so a series anchored before 1970 (common
|
||||
// for yearly birthdays/anniversaries synced over CalDAV) is legitimately
|
||||
// negative — only an *absent* DTSTART marks a malformed row worth dropping.
|
||||
// Dropping negatives made every occurrence of such a series un-openable
|
||||
// (the detail loads the ancient series-master DTSTART), see issue #34.
|
||||
if (isNull(EventDetailProjection.IDX_DTSTART)) {
|
||||
Log.w(TAG, "Dropping event with missing dtstart")
|
||||
return null
|
||||
}
|
||||
val begin = getLong(EventDetailProjection.IDX_DTSTART)
|
||||
|
||||
// Recurring events store DURATION instead of DTEND, so the series row's
|
||||
// DTEND is null. Keep the event (end == begin); callers that opened a
|
||||
|
||||
@@ -10,8 +10,11 @@ import de.jeanlucmakiola.calendula.domain.ics.parseRfc2445DurationMillis
|
||||
* of DTEND — reconstruct the end the same way the `.ics` export does.
|
||||
*/
|
||||
internal fun ColumnReader.toSearchResult(): EventInstance? {
|
||||
// A pre-1970 series anchor is a legitimately negative epoch-millis DTSTART
|
||||
// (see EventDetailMapper / issue #34); drop only a genuinely absent one, so
|
||||
// long-running birthdays/anniversaries still surface in search.
|
||||
if (isNull(SearchProjection.IDX_DTSTART)) return null
|
||||
val dtStart = getLong(SearchProjection.IDX_DTSTART)
|
||||
if (dtStart < 0L) return null
|
||||
val end = when {
|
||||
!isNull(SearchProjection.IDX_DTEND) -> getLong(SearchProjection.IDX_DTEND)
|
||||
else -> dtStart + parseRfc2445DurationMillis(getString(SearchProjection.IDX_DURATION))
|
||||
|
||||
Reference in New Issue
Block a user