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:
@@ -37,16 +37,15 @@ internal fun ColumnReader.toEventDetailCore(
|
|||||||
// Recurring events store DURATION instead of DTEND, so the series row's
|
// Recurring events store DURATION instead of DTEND, so the series row's
|
||||||
// DTEND is null. Keep the event (end == begin); callers that opened a
|
// DTEND is null. Keep the event (end == begin); callers that opened a
|
||||||
// specific occurrence supply the real per-occurrence times from
|
// 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)) {
|
val end = if (isNull(EventDetailProjection.IDX_DTEND)) {
|
||||||
begin
|
begin
|
||||||
} else {
|
} else {
|
||||||
val rawEnd = getLong(EventDetailProjection.IDX_DTEND)
|
getLong(EventDetailProjection.IDX_DTEND).coerceAtLeast(begin)
|
||||||
if (rawEnd < begin) {
|
|
||||||
Log.w(TAG, "Dropping event with dtend=$rawEnd < dtstart=$begin")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
rawEnd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kept raw (no untitled fallback): the detail screen substitutes its own
|
// Kept raw (no untitled fallback): the detail screen substitutes its own
|
||||||
|
|||||||
@@ -120,9 +120,14 @@ class EventDetailMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `dtend before dtstart drops detail`() {
|
fun `dtend before dtstart is clamped to a zero-length event, not dropped`() {
|
||||||
|
// A backwards DTEND is malformed, but dropping it would make the event
|
||||||
|
// un-openable (the "Something went wrong" trap, same as issue #34); keep
|
||||||
|
// it as a zero-length event so the user can still open and fix it.
|
||||||
val detail = detailReader(dtstart = 2000L, dtend = 1000L).toDetail()
|
val detail = detailReader(dtstart = 2000L, dtend = 1000L).toDetail()
|
||||||
assertThat(detail).isNull()
|
assertThat(detail).isNotNull()
|
||||||
|
assertThat(detail!!.instance.start.toEpochMilliseconds()).isEqualTo(2000L)
|
||||||
|
assertThat(detail.instance.end.toEpochMilliseconds()).isEqualTo(2000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user