feat(contacts): mirror contact special dates into local calendars (#15) #57
Reference in New Issue
Block a user
Delete Branch "feat/contact-special-dates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Mirrors contacts' birthdays, anniversaries and other dates into local, per-type calendars ("Birthdays", "Anniversaries", "Other dates") that stay in sync as contacts change. Optional, opt-in under Settings → Contact special dates; the first feature to use
READ_CONTACTS(requested only on enable), fully offline, contacts are read-only. Tracking: Codeberg #15 (builds on #14).What's here
UID_2445, so re-syncs never duplicate and a user's own edits (reminders, notes, location) survive.CAL_SYNC2marker; per-type colour, title template, calendar-wide reminder default, and show-source-year toggle.ContentObserver.Review + hardening
This branch was code-reviewed before merge; the fixes are included:
Correctness
CalendarSource.isManaged(from the durable marker, so it survives a backup restore), are excluded from the new-event calendar picker, and managed-event queries match only our owncontact-UID prefix — so the sync can no longer delete a user event that landed in a managed calendar.sync()/teardown()are serialized by a lifecycle mutex (no double-created calendars, no recreate-after-teardown); the foreground trigger has its own work name (can't cancel-and-swallow an enable sync); a revoked calendar permission parks the feature as stalled instead of retrying forever.Cleanup
data/calendarwith named entries; oneContext.hasContactsPermission(); deduped reminder-override decoding; on-convention dialog text input.Testing
./gradlew :app:testDebugUnitTestgreen (added cases for the next-occurrence offset sampling and the custom-date UID discriminator).release/v2.13.0merged into the branch; conflicts (settings/editor imports,SettingsSectionenum, the renamed colour palette) resolved.🤖 Generated with Claude Code
A dedicated sub-page under Settings drives the mirror: - master enable toggle — requests READ_CONTACTS contextually (only here, never at startup), runs an immediate sync on enable, and on disable confirms then tears the managed calendars down; - per-type toggles (Birthdays / Anniversaries / Other), each confirm-guarded since turning one off deletes its calendar; - an editable title template ({name}/{age}) and a Show-age toggle; - a "Sync now" row with the last-run time, a paused/permission banner with a re-grant button, and a pointer to per-calendar colour/visibility/reminders. Exposed via a separate SpecialDatesUiState flow (the main settings combine is already at capacity); the ViewModel gained the engine/scheduler wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>- German translations for the special-dates sub-page, the managed-field editor hint, and the calendar/title strings. - Store listing (en/de): a paragraph on the optional, on-device, one-way contact special-dates feature. - Soften the calendar-permission rationale ("all it asks for up front") now that an optional contacts permission exists, without weakening the privacy promise. - CHANGELOG entry for #15. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>A FREQ=YEARLY event has one static title, so {age} is only a sync-time snapshot and can look wrong on far-future occurrences. Keep it out of the default templates ({name}'s birthday / anniversary), and when a user does add {age} in the title-format editor, show a short disclaimer about the snapshot behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>A FREQ=YEARLY event has one static title, so a per-occurrence age is impossible without heavy per-year exception rows. Replace {age} with {year} — the birth year (or an anniversary's start year) — which is static and correct on every occurrence, and shows everywhere (widgets, other apps, exports). - renderSpecialDateTitle substitutes {year}; drop the age snapshot computation and the sync `today` parameter. - Default templates become "{name}'s birthday ({year})" / "…anniversary ({year})". - Rename the setting to "Show year" (prefs specialDatesShowYear) and remove the now-unneeded {age} snapshot disclaimer in the template editor. - Update tests, CHANGELOG and the design-doc note. lint + unit tests + assembleDebug green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The mirror keyed each event on (contact, type) only, and the read deduped on that same pair from an unordered query. So a contact with two custom dates ('Wedding', 'Graduation') mirrored just one — and, the winner being order-dependent, the single event could ping-pong between the two dates across syncs. A Custom date's UID now carries a discriminator (its label, else its month-day), the read dedups on the full UID, and the query is ordered by Data._ID so a genuine duplicate resolves the same way every time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>