Files
clockula/docs/ROADMAP.md
T
2026-09-11 16:05:58 +02:00

237 lines
13 KiB
Markdown

# Clockula — roadmap
The **status** view, and the work queue. Design rationale for every decision here
lives in [`PLAN.md`](PLAN.md); the shape of the code as it stands will live in
[`ARCHITECTURE.md`](ARCHITECTURE.md) from M2 onward.
Status legend: ✅ done · 🚧 in progress · ⬜ not started
---
## Current state (one line)
✅ **M3 done.** Clockula's alarms ring. Next-fire resolution is a pure function
over a repeat mask and a zone, DST-correct in both directions; skip and snooze
each have a persisted watermark; the engine registers only the single next alarm
via `setAlarmClock`, is driven by five thin receivers, and rings through a
foreground service that survives process death and reboot and never degrades to
silence. Schema v2 adds `alarm_states`, and M2's reboot blind spot is closed.
**M4** (the app shell — navigation host, four tabs, the live pill, and the ring
screen's UI) is next.
---
## How the loop works through this
One milestone per iteration, auto-continuing. Each iteration:
1. Pick the **first milestone not marked ✅**.
2. Implement it **completely** — every checklist item under it.
3. Build, and run the tests the milestone names. A milestone is not done until
they pass.
4. **Commit** (floret-kit submodule first where it changed, then Clockula's
pointer bump — separate commits, granular, lowercase area prefix).
5. Mark the milestone ✅ here, update **Current state**, and commit that too.
6. Continue to the next milestone.
Rules that hold across every iteration:
- **Never let a Room entity or `@Query` string leak above the data layer**
(`PLAN.md` §0).
- **Never** add a foojay toolchain resolver, anywhere, not even in a comment.
- Commit each finished unit of work; never accumulate a WIP pile.
- If a milestone turns out to be wrong, say so and amend `PLAN.md` — do not
silently build something else.
- `ARCHITECTURE.md` and `CHANGELOG.md` are updated as part of the milestone that
changes them, not in a later cleanup pass.
---
## v1 milestones
### ✅ M0 — Skeleton & pipeline
The project exists, builds, is themed, and ships itself.
- Gradle project: `settings.gradle.kts`, version catalog copied from Agendula
(drop `:provider`, add Room + KSP), AGP 9.x conventions, `vcsInfo` off on
release, no foojay resolver.
- floret-kit as a git submodule + `includeBuild`, with the gitignored
`floret-kit/local.properties` arrangement documented.
- Hilt, Compose, `MainActivity`, `FloretExpressiveTheme` reseeded to `#6B7A5C`
(`ui/theme/Color.kt` + `Theme.kt`), hand-tuned light/dark schemes.
- `core-crash` wired; `core-locale` + `res/xml/locales_config.xml`.
- Codeberg repo, canonical from the first commit. Full pipeline per `PLAN.md` §10:
`ci.yaml`, `translations.yaml`, `release.yaml` (Agendula spine + Calendula `play`
job), `renovate.yml`, issue/PR templates, F-Droid metadata, fastlane tree.
- `README.md`, `LICENSE` (MIT), `CHANGELOG.md`, `.editorconfig`, `.gitattributes`,
`.gitignore`, `docs/README.md` index.
- **Done when:** it builds, installs, shows a themed placeholder, and CI is green.
✅ `assembleDebug`, `lintDebug` and `testDebugUnitTest` all pass;
`scripts/check_reproducible_release.sh` reports every invariant holding.
Two notes for later: the store listing and `fastlane` changelogs are written
but no screenshots exist yet (M11), and `docs/ARCHITECTURE.md` is still
unwritten by design — M2 writes it, once there is an architecture to
describe.
### ✅ M1 — floret-kit: `core-prefs` + `core-di`
Pay the extraction the kit's roadmap has been waiting for a third app to trigger.
- In the kit: `core-prefs` (`ThemeMode`, `dynamicColor`, typed DataStore wrapper,
`toEnum()`) and `core-di` (`@IoDispatcher` + provider), each with tests, module
docs, `CHANGELOG` and `ROADMAP`/`ARCHITECTURE` updates.
- Clockula consumes both. Migrating Calendula and Agendula is explicitly **out of
scope** — note it in the kit's roadmap as a follow-up.
- **Done when:** the kit's tests pass, Clockula builds against the new modules,
and both repos are committed (submodule first, then the pointer bump).
✅ floret-kit 0.4.0. `core-prefs` (56 kit tests) gives the family a typed
`Pref<T>`/`PrefStore` over DataStore; reads degrade to the default instead of
throwing, and `flow()` is distinct-until-changed. The appearance key names are
frozen at what Calendula and Agendula already write, so the sibling migration —
a follow-up in the kit's roadmap, not here — needs no data migration.
`core-di` carries `hilt-compiler` itself, since an `@InstallIn` module only
reaches a consumer's component if its own library was processed; Clockula's
`assembleDebug` is the integration test for that binding. Clockula's theme now
reads the stored preference, with the DataStore built on a corruption handler
so an unreadable prefs file cannot brick startup.
### ✅ M2 — Data layer
The whole storage stack, headless. No UI.
- Room: `alarms`, `timers`, `world_clocks`, `stopwatch_laps` (`PLAN.md` §5),
schema exported and committed, DAOs.
- Domain models (plain Kotlin) + mappers. Repositories exposing Flows.
- DataStore prefs (theme, dynamic colour, defaults, stopwatch running state).
- DI modules.
- **Tests:** mappers, repository behaviour, the elapsed-realtime vs wall-clock
distinction under a simulated clock change.
- Write the first `ARCHITECTURE.md`.
- **Done when:** the schema is exported and committed, no Room type or query
string appears above `data/`, and the build, lint and unit tests pass.
✅ Four tables (`alarms`, `timers`, `world_clocks`, `stopwatch_laps`), schema
v1 exported to `app/schemas` so migrations can be tested from the beginning.
The seam that matters: the app talks to four repository interfaces speaking
domain types and Flows, and `ArchitectureRulesTest` fails the build if Room, an
entity or a query string ever appears above `data/`. Time is a parameter rather
than an ambient read — `WallClock` and `ElapsedRealtimeClock` are distinct
types, so a running timer anchors on the monotonic clock and keeps a wall-clock
value only as a post-reboot fallback, and the stopwatch keeps none at all. That
leaves one honest gap, written down rather than glossed: once a new boot's
uptime climbs past a stored anchor the reboot goes unnoticed, so closing it
needs a persisted boot id and M3's `BOOT_COMPLETED` receiver. Every
read-modify-write runs inside a DAO transaction, and the stopwatch run record
is written whole in one DataStore edit, so concurrent writers cannot swallow
each other's edits or persist a torn record. 151 JVM tests over fake DAOs and
injected fake clocks; the migration test is the only thing needing a device.
### ✅ M3 — Alarm engine
The hard part (`PLAN.md` §4), still headless apart from the ring screen's
plumbing.
- Next-fire resolution: local time-of-day + repeat mask → next instant in the
device zone; `skipNextOccurrence`; snooze.
- `AlarmScheduler` over `setAlarmClock`, single-next-alarm registration,
`USE_EXACT_ALARM` with a `SCHEDULE_EXACT_ALARM` fallback path.
- Receivers: fire, `BOOT_COMPLETED`, `TIME_SET`, `TIMEZONE_CHANGED`,
`MY_PACKAGE_REPLACED`.
- Ringing foreground service: audio focus, `USAGE_ALARM`, volume ramp, vibration,
ringtone URIs; survives process death and reboot mid-ring.
- Full-screen-intent notification + `canUseFullScreenIntent()` handling, degrading
to a high-priority heads-up notification that still rings — never to silence.
- **Tests:** DST spring-forward and fall-back, every repeat configuration, skip,
snooze-vs-next-occurrence. These are the tests the app lives or dies on.
✅ 326 JVM tests, 172 of them new. The engine is a plain Kotlin class that
reaches Android through four interfaces it does not implement, so the
behaviour the app lives or dies on is pinned without an emulator —
`ArchitectureRulesTest` fails the build if `android.*` ever appears in
`domain/`, `AlarmEngine.kt` or `system/`. Occurrences are generated by walking
local dates through `ZonedDateTime.of` rather than by adding 24 hours to an
instant, which is the whole DST story: a 02:30 alarm rings at 03:30 on a
spring-forward night instead of vanishing, and once rather than twice on a
fall-back night. Asserted for Berlin and New York and across all 128 repeat
masks. Two rules keep constant re-resolution honest: a two-minute grace window
so a late fire still counts, and a `handled_occurrence` watermark that can
suppress a re-fire but — because suppression also requires the candidate to be
at-or-before now — can never silence the future. Skip needed a watermark of its
own, since a bare flag eats a second alarm once the skipped occurrence passes.
At most one alarm rings and a newly-firing one takes over, so closing a cycle
touches the shared ring and auto-silence slots only when that alarm is the one
ringing; every write to `alarm_states` runs under one mutex, because a cold
start triggered by the fire broadcast otherwise races its own reschedule pass.
The "never to silence" chain is a testable invariant, not a hope: full-screen
intent, else a heads-up notification that still rings, else the default
ringtone, else forced vibration. Schema v2 adds `alarm_states`, and the boot id
closes M2's blind spot for running timers **and** the stopwatch — a scope move
from M7, recorded in `ARCHITECTURE.md` §5. Knowingly open: the migration and
the other 12 instrumentation tests compile but have never run on hardware, as
no device was attached; an auto-silenced alarm is silently missed, with no
missed-alarm notification until someone asks for one.
### ⬜ M4 — App shell
- Navigation host, four tabs, M3 navigation bar, adaptive rail on wide layouts.
- The **live pill** (`PLAN.md` §9) — shared running-state surface, pause + stop
from any tab.
- The ring screen UI: over-lockscreen, turn-screen-on, snooze/dismiss, optional
dismiss challenge that cannot strand the user.
- Motion from the kit's `identity` module; predictive back.
### ⬜ M5 — Alarms screen
List, create, edit, delete, enable/disable. Time picker, repeat-day selector,
label, ringtone picker, vibrate, snooze settings, per-alarm overrides. Next-fire
line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`.
### ⬜ M6 — Timers
Multiple concurrent timers, presets, labels, add/pause/reset/+1min. Foreground
service with notification controls, expiry ringing reusing M3's audio path.
Elapsed-realtime anchored.
### ⬜ M7 — Stopwatch
Start/stop/reset, laps with splits and cumulative times, best/worst lap emphasis.
Foreground service so it survives backgrounding; laps persist across process
death. This is where the big-readout typography gets settled for the whole app.
The post-reboot repair is **not** here — M3 took it (`ARCHITECTURE.md` §5);
M7 owns only how the paused result is presented.
### ⬜ M8 — World clock
IANA zone list with search, ICU-localised city and zone display names, offset and
day-difference relative to home, reorder, home-zone handling. The analog face
built from `MaterialShapes` / `androidx.graphics.shapes` — the app's one
deliberate showpiece.
### ⬜ M9 — System interop
The full `android.provider.AlarmClock` contract (`PLAN.md` §6): `SET_ALARM`,
`SET_TIMER`, `SHOW_ALARMS`, `SHOW_TIMERS`, `DISMISS_ALARM`, `SNOOZE_ALARM`, plus
next-alarm publishing. Hostile-input validation at the intent boundary.
- **Tests:** extra validation, including malformed and out-of-range input.
### ⬜ M10 — Settings, backup, and the self-check
- Settings composed from kit components: theme, dynamic colour, language,
alarm/timer/clock defaults, about + crash reporting.
- JSON backup export/import via SAF (`PLAN.md` §7), schema documented in the repo.
- The **"why might my alarm not ring?"** self-check screen (`PLAN.md` §4) —
real device state, deep links to the exact settings pages.
- **Tests:** backup round-trip and unknown-field tolerance.
### ⬜ M11 — Release readiness
Translations wired to Weblate, fastlane metadata and screenshots, F-Droid
reproducibility guard verified against the submodule, instrumentation smoke
tests, accessibility pass (TalkBack on the ring screen especially), on-device
reliability soak across a few real nights. Then 1.0.0.
---
## Post-v1
Deferred deliberately, in rough priority order:
- **Dock / screensaver mode** — a `DreamService` full-screen clock. Cheapest big
win, and the surface people look at most.
- **Glance home-screen widget** — next alarm and running timer.
- **Quick Settings tile** — timer control and next alarm from the shade.
- **Bedtime / sleep schedule** — wind-down and wake, with DND handoff. A feature
in its own right, realistically v2.
- **`core-notification` in floret-kit** — revisit once Clockula's real
notification surface is known (`PLAN.md` §3).
- **Migrate Calendula and Agendula onto `core-prefs` + `core-di`** — tracked in
the kit's roadmap, not here.
- **Wear OS companion** — unscoped.