From 6ea11f7bbda1f5289c99df7995f07c3f545c791e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 12 Sep 2026 16:49:05 +0200 Subject: [PATCH] docs: mark M6 done --- docs/ROADMAP.md | 94 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4f7d65d..458924f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,14 +10,15 @@ Status legend: โœ… done ยท ๐Ÿšง in progress ยท โฌœ not started ## Current state (one line) -โœ… **M5 done.** The Alarms tab is real: a live list that says when each alarm -will ring and marks the next one, and an editor that writes as you go โ€” the -Material time picker, a repeat-day row that starts on your locale's own first -day, a ringtone picker with the device's sounds, a file of your own and a Silent -option that still vibrates, and five per-alarm overrides each of which can say -"follow the app default" rather than silently copying it. The other three tabs -are still empty states: **M6** (timers โ€” multiple concurrent, presets, the -foreground service and expiry ringing) is next. +โœ… **M6 done.** The Timers tab is real: several timers at once, a keypad and +saved presets that set one in a couple of taps, pause/reset/"+1 min" from the +row, the live pill *and* a notification whose countdown the system draws, a +foreground service alive exactly while something is active, and expiry that +rings through M3's audio path โ€” waiting its turn when an alarm is already +ringing rather than talking over it. Every countdown is anchored on the device's +uptime, so moving the system clock cannot move a timer in either direction. Two +tabs are still empty states: **M7** (stopwatch โ€” start/stop/reset, laps with +splits, and the app-wide big-readout typography settled at last) is next. --- @@ -248,11 +249,86 @@ line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`. settings screen. The seven new instrumentation tests compile in the gate but have **never run on hardware**, as no device was attached. -### โฌœ M6 โ€” Timers +### โœ… 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. + โœ… 843 JVM tests, 219 of them new, and **still no Robolectric** โ€” kept for a + second screen *and* for a foreground service, which is the other place a + project usually gives in. Every decision is a pure object (`TimerReadings`, + `TimerExpiry`, `TimerRingPolicy`, `TimerNotificationPolicy`, `TimerPresets`, + `TimerDurationEntry`, `TimerListRows`, `TimerRoutes`) or a method on the + engine or one of the two ViewModels, tested over the **real** + `TimerRepositoryImpl` and the **real** `TimerEngine`. `TimerService` holds no + policy and no state beyond one flag โ€” even its two `delay` amounts come from + the engine, so the timings are asserted in a JVM test. Three new + `ArchitectureRulesTest` rules pin it, one of which makes a *second* + `MediaPlayer` a build failure. The four new instrumentation tests bring the + compiled total to 32. + + The decisions worth knowing: the expiry slot is **one** registration on the + `ELAPSED_REALTIME_WAKEUP` base โ€” so a `TIME_SET` cannot warp a running timer, + and a timer never populates `getNextAlarmClock()`, which belongs to the user's + next *alarm* โ€” and its fire is an **idempotent sweep carrying no id**, so a + late delivery expires everything due, an early one writes nothing, and the + anchors *are* the watermark; the service adds a second, independent trigger + for the same sweep, because neither has to be reliable alone; one + `systemExempted` foreground service covers the countdown and the ring, alive + on the live pill's own "active, not running" predicate, and the countdown + holds **no wake lock** (the alarm slot is what wakes the device); an alarm + ringing **wins the audio** and the timer's ring is *deferred, not lost*, + because the arbitration is a pure function of current state rather than an + event โ€” so the reverse case is symmetric and free; two timers expiring + together share one ring session and one ten-minute window, and each is stopped + on its own, because silently resetting both destroys the information "which + one finished"; auto-silence stops the noise and leaves the row EXPIRED, which + is the load-bearing difference from an alarm; presets are app-wide in + DataStore with out-of-range values **dropped, never clamped**; and the + notification's countdown is the platform chronometer, so the service posts + once per state change rather than once per second for forty-five minutes. + + Two things the checklist did not name were changed anyway, and are recorded in + `ARCHITECTURE.md`: **"+1 min" on an expired timer now resumes it** with + exactly the extra, in one transaction (M2 left it paused, written before any + UI existed and pinned by no test โ€” and resuming is the only way to avoid + emitting an intermediate PAUSED frame to the pill and the row); and **the live + pill's timer actions now go through `TimerEngine`**, because after M6 there is + an AlarmManager registration and a service that have to move with the state. + The pill's contract is otherwise unchanged: M6 *extracted* its precedence into + `TimerReadings` so the pill, the notification and the ring share one ordering, + and `LivePillSelectorTest` passing **unmodified** is the proof. + + No schema change (the database stays at v2; the ring session's one volatile + fact is a DataStore record, which is `PLAN.md` ยง5's own record-versus-rows + split and keeps it out of M10's backup), **no new permission** (a timer never + takes over the screen โ€” no full-screen intent, no ring activity), and **no + floret-kit change**: the kit earned `CollapsingScaffold`'s + `floatingActionButton` slot at 0.6.0 for exactly this shape of tab, and M6 + consumes it. + + Knowingly open: no hand reordering of timers (`sort_order` and + `TimerRepository.reorder` stay caller-less, as they do for alarms), no + per-timer vibrate, volume-ramp or dismiss-challenge overrides (`timers` has + one nullable settings column and four more would be a schema change), no + "stop all" when several timers are expired, no undo after a delete (traded for + a confirmation, as with alarms), no ramp on a timer's ring, no presets + management screen and no editing of `default_timer_duration_millis` or + `default_timer_ringtone_uri` โ€” M10's settings screen. `TimerSnapshot.anchorIsStale` + is still not surfaced in the UI. Of `AlarmClock`'s intent contract only the + in-process hook `ShellNavigation.tabForAction` exists, wired to one action; + M9 owns the rest. The four new instrumentation tests compile in the gate but + have **never run on hardware**, as no device was attached โ€” the same caveat + M5's seven carry. + + Two cases in the new suite are left **red** and are a design conversation + rather than a behaviour gap; both are defects in the test, not in the code: + `TimerIntentsTest` ยง5.17 #3 asserts `containsNoDuplicates` over a list that + contains the *same* `requestCodeFor(REQUEST_PAUSE, 1L)` call twice by + construction, which no deterministic function can satisfy; and `TimerPrefsTest` + ยง5.10 #11 opens a second DataStore over a live file in the same scope, which + DataStore refuses by design โ€” as `SettingsPrefsTest` itself documents. + ### โฌœ 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