All-day events live at UTC midnight, so a raw "1 day before" reminder fires at an off hour (02:00 local in CEST) rather than the morning. Add a global "all-day reminder time" setting (default 09:00) and encode it into the provider MINUTES offset so the reminder lands at the chosen wall-clock time the day before instead. - AllDayReminderEncoding: pure to/from provider-minutes helpers, keeping the form/UI/diff in whole-day "semantic" minutes and converting only at the Reminders read/write boundary (insertEvent, reconcileReminders, EventDetailMapper). Covers DST, negative offsets, and pre-existing rows. - SettingsPrefs.allDayReminderTimeMinutes (default 540) threaded from the repository into the data-source write paths. - Settings: a time-picker row, plus a shared TimePickerAlert lifted from the event editor. - Fix the time picker's 12/24-hour detection: honour an explicit system override, else fall back to the device locale rather than the app's per-app language, so it matches the rest of the device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 KiB
Calendula — Roadmap
v0.x — Pre-Release
| Version | Milestone | Status |
|---|---|---|
| v0.1 | Foundation & CI | complete |
| v0.2 | Data Layer & Permission Flow | complete |
| v0.3 | Month + Week + Day views, view switcher | complete |
| v0.4 | Event Detail (S4) + humanized recurrence | complete |
| v0.5 | Calendar filter (M3) + Settings (M4) | complete |
| v0.6 | Full event read — surface every readable field | complete |
| v1.0 | First public release — polish pass, F-Droid | complete |
Delivery ran ahead of the original table: Day view (S3) shipped in v0.3 and Event Detail (S4) in v0.4, so the Filter/Settings milestone became v0.5.
Jump-to-date (the date-picker half of M2) was cut from scope and will not ship. The "Today" half of M2 already shipped in v0.5 (drawer entry).
v0.6 — Full event read
Round out the read-only model so a detail view shows everything the system
actually stores, before write support starts. Scope = CalendarContract
columns we don't yet read/display:
- Reminders (
VALARM) — readCalendarContract.Reminders, list lead times - Status — Confirmed / Tentative / Cancelled (cancelled shown struck-through)
- Availability (
TRANSP) — Free / Busy chip - Attendee extras — role (required / optional / organizer) + the user's own
SELF_ATTENDEE_STATUS - Timezone (
EVENT_TIMEZONE) — shown only when it differs from the device zone - URL —
tappable link cardcut:CalendarContractexposes noEvents.URLcolumn (onlyCUSTOM_APP_URI, an originating-app deep-link). URLs are instead surfaced by linkifying the description text - Access level / class (private / confidential) — small chip (optional, trivial)
All of the above shipped in v0.6.0 (2026-06-11).
Deliberately out of v0.6:
- Recurrence exception / modified-occurrence badges —
Instancesalready resolves correct per-occurrence times for display; this only matters for editing, so it folds into v2 CATEGORIES,ATTACH— not reliably exposed byCalendarContract(provider limitation, not our choice)
v1.0 — First Public Release — shipped 2026-06-11
All V1 features shipped, polished, on F-Droid. Read-only calendar. Cut directly after v0.6 (full event read) plus the onboarding-screen polish pass.
Polish backlog (pre-1.0)
Redesign the initial grant-access (permission) screen— done (Material 3 Expressive onboarding, shipped in v0.6.0 / v1.0.0)
v2.0 — Write Support (complete, shipped 2026-06-11)
Delivered in four releasable slices (plan:
docs/superpowers/plans/2026-06-11-03-write-support.md). The V1 spec is a
guide here, not a contract — scope per slice is decided as we go.
| Version | Milestone | Status |
|---|---|---|
| v1.1 | Write foundation — WRITE_CALENDAR, read-only-calendar detection, delete (series + single occurrence) |
complete (shipped 2026-06-11) |
| v1.2 | Create event — form, FAB, last-used-calendar preselect | complete (shipped 2026-06-11) |
| v1.2.1 | Form polish after on-device review — card design system, optional fields + settings defaults, OptionCard dialogs, expressive motion | complete (shipped 2026-06-11) |
| v1.3 | Edit event — shared form, scoped recurring writes (this / following / all), recurrence picker | complete (shipped 2026-06-11) |
| v1.4 | Reminder notifications — see below | complete (shipped 2026-06-11) |
| v2.0 | Conflict dialog, polish pass (store copy refresh, F-Droid screenshots), release | complete (shipped 2026-06-11) |
v2.0 scope was re-cut on 2026-06-11, after v1.4:
- Occurrence edit already shipped early, in v1.3.
- Quick-add is cut from scope: the full form already opens prefilled (visible day, last-used calendar, optional fields hidden), so the sheet would only save one screen transition while adding a second create-surface to maintain. Revisit only if real-world feedback says creation feels heavy.
- Calendar switching while editing moves to the v3 backlog (sync-adapter
minefield:
CALENDAR_IDis sync-adapter-owned, AOSP locks the field; an honest implementation is copy+delete like Google Calendar, with sync-identity and attendee side effects). - Conflict dialog stays (plan 03, decision 5): on save, compare against the row as it was when the form loaded; on external change, ask overwrite / discard. Closes the silent-clobber gap on synced calendars.
v1.4 — Reminder Notifications
Essential, not nice-to-have: Calendula targets users for whom it is their
only calendar app, so reminder delivery can't be delegated to Google/OEM
Calendar. The calendar provider schedules reminders and broadcasts
android.intent.action.EVENT_REMINDER, but it does not post the visible
notification — a calendar app must. We become that app (the Etar model).
Scope:
- Manifest-registered
BroadcastReceiverforEVENT_REMINDER(data schemecontent://com.android.calendar) — wakes us at reminder time, no foreground service. - Read
CalendarContract.CalendarAlerts/Reminders, filter toMETHOD_ALERT/METHOD_DEFAULT(skipMETHOD_EMAIL); post on a dedicated notification channel; tap opens event detail. POST_NOTIFICATIONSruntime permission (API 33+) — requested in onboarding.- Onboarding step: (a) request
POST_NOTIFICATIONS, (b) in-app reminders toggle, default ON, with copy warning that a second calendar app with notifications on will cause duplicate reminders. Mirrored into Settings (reversible).
Deliberately deferred (add only if needed):
- Snooze / dismiss notification actions (Etar has them)
- Battery-optimization exemption prompt for delivery reliability
v2.1 — Month event grid + drawer view tabs (shipped 2026-06-15)
- Month grid shows real events as continuous multi-day bars (not just dots)
- View section in the navigation drawer to switch Month / Week / Day
- Fix: text cursor no longer jumps in event text fields
v2.2 — Tap-to-create + local calendar management (shipped 2026-06-16)
- Tap an empty slot in day/week → create form prefilled with that day + the tapped hour (snapped to the hour, 1 h long)
- Local (device-only) calendar management in a full-screen editor from
Settings → Calendars: create / rename / recolor / delete, with name,
pastel-previewed colour, and description (stored in
CAL_SYNC1) - Synced calendars listed read-only, grouped by account, each with a per-account "manage in source app" deep-link (resolved from the account's authenticator — DAVx5/ICSx5/…) + an add-account shortcut
- Shared
InlineTextFieldextracted toui.common(event form + calendar editor share one input style)
v2.3 — Material 3 grouped-list redesign (shipped 2026-06-16)
A structural + visual pass adopting one shared blueprint (modelled on the ReFra gallery app) across Settings, the calendar manager and the navigation drawer.
- Shared
ui/common/GroupedList.kt:CollapsingScaffold(aLargeTopAppBarwhose title collapses on scroll) +GroupedRow(Position-based corner grouping, press-animated corners,selected+minHeightknobs). - Settings: category hub with About card on top and sliding sub-pages
(Appearance / New event form / Notifications); theme/week-start/language
pickers moved from
DropdownMenuto OptionCard dialogs; token-based icon chips;ic_gitea.xmlfor the About "Source" button. - Calendar manager + drawer restyled to match; shared
CalendarColorChip; drawer scrolls as one with the active view highlighted. - Cards use
surfaceContainerHighfor readable contrast. - Donate button on the About card deferred (target TBD).
Backlog (theme-based, post-v2.1)
The old v3.0 / "daily-driver polish" / "Locations & People" lists are consolidated here by theme. Within a group, (in progress) / (next) mark what is being or about to be worked; everything else is an approved-but-unscheduled idea unless tagged (idea) / (go/no-go) / (rejected). Order across groups is not a commitment.
Near-term sequence (ranked, 2026-06-16)
The theme groups below are the full menu; this is the committed order for the next stretch. Ranking favours finishing the current create/edit + calendar arc before opening new fronts, then cheap-relative-to-value items and ones that unblock a later item. Order is a plan, not a contract — revisit after each lands.
Tier 1 — finish the current arc (create/edit + calendars)
- Tap-to-create in day/week (shipped v2.2.0) — prefilled create from an empty slot
- Local calendar management + "manage in source app" deep-links (shipped v2.2.0)
Settings redesign & restructure(shipped v2.3.0 — grew into the full grouped-list blueprint across Settings + calendars + drawer; see "v2.3" above)Per-event color(shipped v2.4.0) — palette calendars writeEVENT_COLOR_KEY(sync-safe); local/opted-in calendars write a rawEVENT_COLOR; off-by-default setting for no-palette synced calendars Tier 1's create/edit + calendars arc is effectively closed. Duplicate event was deprioritised (2026-06-17) as low-importance and dropped to the bottom of the sequence; the next item is now Jump-to-date (formerly Tier 2).
(Tier 2+ numbering below shifts accordingly; ranking unchanged.)
Settings redesign & restructure (shipped v2.3.0)
The original scope below is kept as a record; the implementation expanded from a sub-screen restructure into the shared grouped-list blueprint (see "v2.3" above).
The settings screen has grown into a flat vertical scroll of divider-separated sections (Appearance, Event form, Notifications, Calendars, Language, About) and will keep accreting rows (per-event-color defaults, default reminder, more calendar entries are all queued). It needs structure before it gets unwieldy.
Decided (2026-06-16): sub-screens, not flat-but-carded. The top level becomes a category list; each category opens its own destination. More M3-idiomatic for a settings surface that will keep growing, and it mirrors the existing Calendars row, which already navigates out to its own screen.
Structure — top-level settings list → category destinations:
- Appearance → theme, dynamic colour, week start
- Event form → the 6 default-field toggles + the hint text
- Notifications → reminders toggle (POST_NOTIFICATIONS flow stays)
- Calendars → already its own screen (
CalendarsScreen); just becomes a peer category row, no change to that screen - Language → single control; keep as a top-level row that opens an OptionCard directly (a whole sub-screen for one choice is overkill)
- About → kept inline on the top-level list as a card (read-only info,
not worth a navigation hop). Card layout, top → bottom:
- Identity — app logo + name "Calendula", with "by Jean-Luc Makiola" as a subtitle beneath the name
- Action buttons (small, button-styled, sit in a row):
- Source — Gitea logo, opens the repo (
about_source_url) - License — opens the LICENSE file on Gitea
- Donate (tentative) — sits next to Source; target TBD (decide before building: Liberapay / Ko-fi / Gitea sponsor / etc.)
- Source — Gitea logo, opens the repo (
- Version — small version number at the bottom of the card
Scope:
- Navigation — add the settings sub-screen destinations alongside the
existing settings/calendars routes in
CalendarHost; back pops to the settings list (mind the existingBackHandlerthat guards against falling through to the activity). - Fix the dialog-pattern violation — theme, week-start and language use
DropdownMenu; the project default is the full-width tonal OptionCard modal (radio/dropdown/text-list dialogs are banned, seeoption-card-modal-style-default). Migrate these selectors to OptionCard. - Visual pass — top-level category rows with leading icons; consistent spacing and row affordances aligned with the event-form card design system.
Out of scope (no new settings features here) — this is a structure + style pass on the existing controls; new toggles ride in with their own features.
Tier 2 — navigation & daily-driver completeness
5. Jump-to-date — drawer date picker (un-cut from V1); cheap, fills the nav gap (done, v2.5.0)
6. Agenda view — the missing 4th view; serves daily-driver users and becomes the data source for the widget (done, v2.5.0)
Tier 3 — platform reach (depends on Tier 2)
7. Home-screen widget — built on the agenda data source from #6 (done, v2.5.0 — agenda + month widgets)
8. App shortcuts: launcher long-press → New event (done, v2.5.0); optional quick-settings tile still open
Tier 4 — reliability, data-safety & interop (re-ranked 2026-06-17)
9. Reminders — defaults + delivery reliability (next) — global default
reminder + per-calendar override, bundled with exact-alarm / battery
hardening. Elevated above .ics: it's core to the "Calendula is your only
calendar app" promise. Full sketch in "Reminders — defaults & delivery
reliability" below.
10. Local-calendar backup / export — device-only calendars have no sync and
therefore no backup; losing the phone = total data loss. Whole-calendar
.ics export + restore. A data-integrity gap, not a feature; front-runs and
overlaps the single-event .ics work below.
11. Share event as .ics + receive/open .ics into a prefilled create form
12. Drag & drop rescheduling in day/week — big-ticket, own slice (recurring drops reuse the scope dialog)
13. Snooze / dismiss notification actions — follows the reminders slice (#9)
Gated — explicit go/no-go before any work (mostly INTERNET-permission calls)
- Remote calendar create/edit (re-implements DAVx5; INTERNET + credential storage)
- Locations & People — contact address picker (no-permission, one-shot) is the safe entry; OSM autocomplete needs INTERNET
- Move event to another calendar — sync-adapter minefield (copy+delete model)
Bottom — deprioritised, not important
- Duplicate event (detail action → prefilled create form) — moved here 2026-06-17; cheap but low value, pick up only if asked
Unranked / fill-in — pinch-to-zoom time scale, tablet/foldable layouts, full-text search, ICS file import. Pulled in opportunistically, not sequenced.
Debatable calls worth a second look: whether local-calendar backup (#10) should lead Tier 4 outright (it's a silent data-loss risk, not a feature); whether drag-drop (#12) jumps ahead given its daily-driver impact.
Navigation & views
Tap an empty slot in day/week → create form prefilled with that date+time, snapped to the hourshipped v2.2.0 (long-press variant not added — single tap covers it)- Agenda view (fourth view: upcoming events grouped by day; also the natural data source for a future widget)
- Jump to date — drawer date picker (un-cut from V1)
- Current-time "now" line in day/week — standard in every calendar, cheap, currently absent. Daily-driver polish.
- Week numbers in the month grid — week view already shows the badge
(
WeekNumberBadge,WeekScreen.kt); extend to month for ISO/European users. - Pinch-to-zoom time scale in day/week
- Tablet / foldable layouts (was v3.0)
- Full-text search (was v3.0) — promote out of "fill-in": for a daily driver with real event history, finding an event is core completeness, not optional.
Event editing & creation
- Drag & drop rescheduling in day/week (recurring drops reuse the scope dialog) — big-ticket, own slice
- Duplicate event (detail action → prefilled create form)
- Per-event color (
Events.EVENT_COLOR, OptionCard picker in the form) (next) — chosen to follow the in-progress tap-to-create + calendar management work: reuses the color-picker component and palette plumbing being built for local calendar management, and finishes the create/edit theme.EVENT_COLOR/EVENT_COLOR_KEYfrom the calendar's color list (Colorstable,TYPE_EVENT); falls back to the calendar color when unset.
Calendars & accounts
Create / manage local (device-only) calendarsshipped v2.2.0 — name + color + description; rename / recolor / delete the calendars the app owns. Inserted underACCOUNT_TYPE_LOCALas a sync adapter; description inCAL_SYNC1. Full-screen "Calendars" editor reached from Settings.Per-calendar "manage in source app" deep-linkshipped v2.2.0 — for synced calendars, open the app the calendar actually came from based on itsACCOUNT_TYPE(DAVx5bitfire.at.davdroid, Googlecom.google, …); fall back to system account/sync settings. Plus an "add account" entry into system Accounts. Honest boundary for remote calendars.- Remote calendar create/edit (go/no-go) — creating a CalDAV
collection (
MKCALENDAR) or a Google calendar means an in-app sync client: INTERNET permission, credential storage, the full server round-trip — i.e. re-implementing DAVx5. DAVx5 exposes no public intent to delegate the create to it. Cosmetic local edits (color/name) to an existing synced row are possible but don't propagate to the server and may be overwritten on next sync — not promised. Same explicit go/no-go gate as the OSM/INTERNET item below. - Move event to another calendar (copy+delete model with a consequences
warning — deferred from v2.0;
CALENDAR_IDis sync-adapter-owned) (was v3.0) - Local-calendar backup / export (Tier 4 #10) — device-only
(
ACCOUNT_TYPE_LOCAL) calendars are first-class in Calendula but have no sync and therefore no backup: a lost/wiped phone destroys them permanently. Whole-calendar.ics(VCALENDAR) export to a user-chosen file (SAF), plus restore-on-import that recreates events into a chosen local calendar. Reuses the .ics serializer from the single-event share work; the restore path reuses the import parser. A data-integrity obligation, not a feature.
Reminders — defaults & delivery reliability (implemented 2026-06-17, feat/default-reminders — pending on-device review)
Two themes bundled because both are "make reminders trustworthy" — the core of the "Calendula is your only calendar app" promise.
Built in this slice (A + the safe half of B): global timed default reminder
- a separate all-day default (day-scale lead times) + per-calendar override
(timed events), applied on create with manual-edit / calendar-switch / all-day-
toggle handling; three pickers + per-calendar override list in Settings →
Notifications; battery-optimisation exemption row (status + system deep-link, no
extra permission).
resolveDefaultReminder+ prefs round-trips unit-tested. Resolution model: all-day events use the all-day global default outright; per-calendar overrides govern timed events only. Reviewed (8-angle), fixes applied: form-reset state race, label-fn consolidation with the detail screen, inline wrapper + single combined flow read.
Deliberately deferred (documented decisions, not oversights):
- Absolute time-of-day for all-day reminders — the all-day default is still minutes-before-midnight (day-scale presets), not "9am the day before" (open decision #2's richer half). Per-calendar all-day overrides also deferred.
- Self-scheduled alarms — kept the existing provider-broadcast architecture
(open decision #1). The battery exemption is the reliability lever; no
AlarmManager/USE_EXACT_ALARMsubsystem was added. - Test-reminder diagnostic and battery prompt inside onboarding — the exemption lives only in Settings for now (onboarding flow untouched to keep the change reviewable).
A. Default reminders (global + per-calendar override)
No provider backing. CalendarContract has no column that auto-applies a
default reminder per calendar — Google's per-calendar defaults live server-side.
So both the global default and the per-calendar override are app-side
preferences, applied by us at event-insert time. We inherit nothing from the
synced calendar.
- Storage (DataStore):
defaultReminderMinutes: Int?— global default;null= "no reminder".defaultAllDayReminderMinutes: Int?— separate all-day default (all-day reminders are expressed as minutes before midnight / day-before-at-time, not minutes before a start instant — they need their own value).perCalendarReminderOverride: Map<Long, Int?>— keyed by calendar id; absent key = inherit global, explicitnull= "no reminder for this calendar". (Same for an all-day override map if we want per-calendar all-day.)
- Apply on create: a fresh event prefills its reminders list from override-or-global for the preselected calendar. Changing the calendar in the form re-applies the new calendar's default only if the user hasn't manually edited the reminders — track a dirty flag, mirroring the per-event-color reset pattern (v2.4).
- Edit semantics: defaults apply to new events only; never rewrite reminders on existing events on open or on calendar-switch-during-edit.
- Settings UI (Notifications sub-page):
- Global default via OptionCard (None / at time of event / 5 / 10 / 15 / 30 min / 1 h / 1 day / custom), plus the separate all-day default.
- Per-calendar overrides: a row per writable calendar (in the Calendars screen or a Notifications subsection), each opening the same OptionCard with a leading "Use global default" option.
B. Delivery reliability (exact alarms + battery)
The provider broadcasts EVENT_REMINDER, but on modern Android (Doze / OEM
battery managers) delivery can be silently delayed or dropped. v1.4 deferred this;
it directly undermines the feature's premise, so it rides in here.
- Exact alarm — decision first: trust the provider broadcast, or
self-schedule via
AlarmManager.setExactAndAllowWhileIdlefor reliability? If we self-schedule, declareUSE_EXACT_ALARM(API 33+, auto-granted for calendar/alarm-category apps, F-Droid-clean) with aSCHEDULE_EXACT_ALARMfallback for API 31–32 (user-revocable → settings deep-link prompt). - Battery-optimization exemption: a soft, optional prompt via
ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS(settings deep-link — never the auto-grant intent), honest copy: "Android may delay reminders to save battery; exempt Calendula for on-time delivery." Shown once after the existingPOST_NOTIFICATIONSonboarding step, reversible in Settings → Notifications. - Diagnostics: a "send a test reminder in 1 minute" button in Notifications settings so users can verify delivery on their specific OEM (Samsung / Xiaomi are notorious for suppressing it).
Open decisions (resolve before building)
- Self-schedule via
AlarmManagervs trust the provider broadcast (reliability vs simplicity + battery cost). - All-day reminder representation (minutes-before vs absolute time-of-day).
- Where per-calendar overrides live in the UI (rows on the Calendars screen vs a list inside the Notifications sub-page).
Later (round two)
- Snooze + dismiss actions on the notification (snooze needs an exact-alarm / WorkManager decision) — Tier 4 #13.
Sharing & interop
- Share event as .ics + open/receive .ics into a prefilled create form (front-runs the import below)
- ICS file import (drag-and-drop) (was v3.0, optional)
Platform & launchers
Home-screen widgetshipped v2.5.0 — agenda + month widgetsApp shortcuts (launcher long-press → New event)shipped v2.5.0 — optional quick-settings tile still open
Quality & reliability
- Accessibility pass — TalkBack content descriptions across all screens, dynamic-type / large-font reflow, touch-target audit. Quality bar for an F-Droid app; nothing tracks it yet.
- Reminder delivery reliability — exact alarms + battery-optimization exemption; specced in the "Reminders — defaults & delivery reliability" slice above (Tier 4 #9).
Locations & People (go/no-go, captured 2026-06-11)
Beyond classic calendar-client scope; discussed, deliberately not planned in detail yet:
- Contact address picker for the location field via the system picker
(
ACTION_PICKon postal addresses) — one-shot, needs no READ_CONTACTS, fits the privacy story. Same mechanism later for picking emails. - OSM address autocomplete in the location field (type "Brandenburger Tor" → tap suggestion → resolved address inserted). Backend would be Photon (Nominatim's public policy forbids autocomplete). Requires the INTERNET permission — first dent in the "no network access" promise; if built: opt-in (off by default), honest copy, configurable endpoint for self-hosters, onboarding footnote + F-Droid copy reworded. This trade-off is an explicit go/no-go decision before any work starts.
- Inline contact suggestions while typing (needs READ_CONTACTS) — only if the picker proves clunky.
- Attendee editing / invites from contacts — own milestone; writing
Attendeesrows touches sync-adapter invitation behavior (Google vs DAVx5 differ).
Consciously rejected
- Travel time / weather / smart suggestions (network, core-promise conflict)
- Natural-language quick entry (high effort, locale-fragile; the prefilled form already covers fast entry)
- Quick-add sheet (the prefilled full form already covers it — cut in v2.0)