fix(detail): clamp a backwards DTEND instead of dropping the event (#34)

toEventDetailCore returned null when a present DTEND preceded DTSTART, the
only remaining false-drop that surfaces as the generic "Something went
wrong." error screen — the same un-openable trap as the pre-1970 DTSTART
bug, and worse because the user can't even open the malformed event to fix
it. Clamp the end to DTSTART (a zero-length event) instead, matching how
SearchMapper already coerces its end. After this the detail mapper drops a
row only when DTSTART is genuinely absent (unrenderable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 17:11:51 +02:00
parent 1a3e4f501f
commit 76846420a2
2 changed files with 13 additions and 9 deletions

View File

@@ -37,16 +37,15 @@ internal fun ColumnReader.toEventDetailCore(
// Recurring events store DURATION instead of DTEND, so the series row's
// DTEND is null. Keep the event (end == begin); callers that opened a
// specific occurrence supply the real per-occurrence times from
// CalendarContract.Instances. Only a present-but-backwards DTEND is malformed.
// CalendarContract.Instances. A present-but-backwards DTEND is malformed,
// but dropping the row would make the event un-openable — the same trap as
// the pre-1970 DTSTART bug above (issue #34): it would surface as the
// generic error screen with no way to open the event and fix it. Clamp to a
// zero-length event instead (matching SearchMapper's coerceAtLeast).
val end = if (isNull(EventDetailProjection.IDX_DTEND)) {
begin
} else {
val rawEnd = getLong(EventDetailProjection.IDX_DTEND)
if (rawEnd < begin) {
Log.w(TAG, "Dropping event with dtend=$rawEnd < dtstart=$begin")
return null
}
rawEnd
getLong(EventDetailProjection.IDX_DTEND).coerceAtLeast(begin)
}
// Kept raw (no untitled fallback): the detail screen substitutes its own