fix: cancel only the tapped occurrence on single-instance recurring delete (Codeberg #47) #68

Merged
makiolaj merged 1 commits from fix/recurring-single-delete into release/v2.14.1 2026-07-12 09:09:07 +00:00
Owner

Fixes Codeberg #47https://codeberg.org/jlmakiola/calendula/issues/47
(reported on v2.14.0, Pixel 9a / Android 17).

The bug

"Delete only this event" on a recurring series did the inverse of what it
promised: it wiped every other occurrence, left the tapped one behind as a
"cancelled" ghost, and re-deleting toggled the whole series back.

Root cause

AndroidCalendarDataSource.deleteOccurrence wrote a cancelled exception carrying
only ORIGINAL_INSTANCE_TIME + STATUS_CANCELED. Without DTSTART + DURATION
the provider clones the master with its RRULE intact and cancels the whole
clone — the exact trap the edit path already documents (Codeberg #16, see
buildOccurrenceExceptionValues KDoc). The single-instance delete path never got
the same discipline.

Fix

  • Anchor the exception as a single instance — DTSTART + DURATION + timezone +
    all-day, read from the series row — so the provider clears the inherited RRULE
    and cancels only that occurrence. Extracted into a pure
    buildOccurrenceCancelValues helper with JVM tests.
  • Filter STATUS_CANCELED out of the instances() grid query (keeping NULL
    status — a normal event) so the cancelled occurrence disappears instead of
    lingering as a tappable ghost. This is what kills symptoms #2 (cancelled label)
    and #3 (restore-on-re-delete).

The other two scopes ("this and following", "all events") were already correct
and are untouched.

Testing

  • ./gradlew test lint green locally; added EventWriteMapperTest coverage for
    the new helper.
  • ⚠️ Not yet on-device verified — the provider write behaviour and the grid
    STATUS filter both need a real device before merge/release.

🤖 Generated with Claude Code

Fixes **Codeberg #47** — https://codeberg.org/jlmakiola/calendula/issues/47 (reported on v2.14.0, Pixel 9a / Android 17). ## The bug "Delete **only this event**" on a recurring series did the inverse of what it promised: it wiped every *other* occurrence, left the tapped one behind as a "cancelled" ghost, and re-deleting toggled the whole series back. ## Root cause `AndroidCalendarDataSource.deleteOccurrence` wrote a cancelled exception carrying only `ORIGINAL_INSTANCE_TIME` + `STATUS_CANCELED`. Without `DTSTART` + `DURATION` the provider clones the master **with its RRULE intact** and cancels the whole clone — the exact trap the edit path already documents (Codeberg #16, see `buildOccurrenceExceptionValues` KDoc). The single-instance delete path never got the same discipline. ## Fix - Anchor the exception as a single instance — `DTSTART` + `DURATION` + timezone + all-day, read from the series row — so the provider clears the inherited RRULE and cancels **only** that occurrence. Extracted into a pure `buildOccurrenceCancelValues` helper with JVM tests. - Filter `STATUS_CANCELED` out of the `instances()` grid query (keeping `NULL` status — a normal event) so the cancelled occurrence disappears instead of lingering as a tappable ghost. This is what kills symptoms #2 (cancelled label) and #3 (restore-on-re-delete). The other two scopes ("this and following", "all events") were already correct and are untouched. ## Testing - `./gradlew test lint` green locally; added `EventWriteMapperTest` coverage for the new helper. - ⚠️ **Not yet on-device verified** — the provider write behaviour and the grid STATUS filter both need a real device before merge/release. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
makiolaj changed target branch from main to release/v2.15.0 2026-07-12 08:50:24 +00:00
makiolaj changed target branch from release/v2.15.0 to release/v2.14.1 2026-07-12 08:53:31 +00:00
makiolaj added 1 commit 2026-07-12 08:53:31 +00:00
fix: cancel only the tapped occurrence on single-instance delete (#47)
All checks were successful
CI / ci (pull_request) Successful in 6m21s
2218c11d3f
"Delete only this event" on a recurring series wrote a cancelled
exception carrying just ORIGINAL_INSTANCE_TIME + STATUS_CANCELED. Without
DTSTART + DURATION the provider clones the master *with its RRULE intact*
and cancels the whole clone, so every other occurrence vanished, the
target survived as a "cancelled" ghost, and re-deleting toggled the
series back — exactly the reported corruption.

Anchor the exception as a single instance (DTSTART + DURATION + zone +
all-day, read from the series row) so the provider clears the inherited
RRULE and cancels only that occurrence — the same discipline the edit
path already documents (Codeberg #16). Also filter STATUS_CANCELED out of
the instances grid query so the cancelled occurrence disappears instead
of lingering as a tappable ghost (NULL status is kept — a normal event).

Extracts the exception ContentValues into a pure buildOccurrenceCancelValues
helper with JVM tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
makiolaj merged commit d398c72005 into release/v2.14.1 2026-07-12 09:09:07 +00:00
makiolaj deleted branch fix/recurring-single-delete 2026-07-12 09:09:08 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: makiolaj/calendula#68