docs: mark M6 done

This commit is contained in:
2026-09-12 16:49:05 +02:00
parent 319d176ae8
commit 6ea11f7bbd
+85 -9
View File
@@ -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