diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 458924f..1d0c0c9 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,15 +10,18 @@ Status legend: โœ… done ยท ๐Ÿšง in progress ยท โฌœ not started ## Current state (one line) -โœ… **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. +โœ… **M7 done.** The Stopwatch tab is real: start, pause and reset, a readout to +hundredths whose fraction is drawn smaller than its seconds, laps with their +split and their running total newest-first with the fastest and slowest marked, +the lap in progress as its own counting row, and a silent `specialUse` +foreground service whose Lap and Pause work with the app closed โ€” its ticking +drawn by the system, so it costs no battery to keep accurate. A stopwatch that +meets a reboot comes back **paused at what it banked**, with every lap intact, +rather than inventing the time it lost; a clock change cannot move it at all. +M0's promise that the big-readout typography would be settled against a working +stopwatch is kept, and every readout in the app now sits in fixed columns. One +tab is still an empty state: **M8** (world clock โ€” the zone list, ICU names, +offsets and the analog face) is next. --- @@ -329,13 +332,85 @@ Elapsed-realtime anchored. ยง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 +### โœ… 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. + โœ… 968 JVM tests, 125 of them new, and **still no Robolectric** โ€” kept now for + a third screen and a third service. Everything the tab decides is a pure + object (`StopwatchFormat`, `StopwatchReadings`, `LapStats`, `StopwatchLaps`, + `StopwatchNotificationPolicy`, `StopwatchRows`) or a method on the engine or + the ViewModel, tested over the **real** `StopwatchRepositoryImpl` across the + real `appendLap` transaction and a real DataStore under a `@TempDir`. + `StopwatchService` holds no policy and **no state at all** โ€” not even the + timers' one `alreadyAlerted` flag, because nothing here ever alerts โ€” and it + has no `delay`. Two new `ArchitectureRulesTest` rules and two extended; the + four new instrumentation tests bring the compiled total to 36. + + The decisions worth knowing: the foreground service is **`specialUse`**, not + `systemExempted`, because a stopwatch is not alarm functionality โ€” it + schedules nothing, wakes nothing and rings nothing, and claiming the type + would have the app tell the platform something untrue; it holds **no wake + lock, ever**, since the elapsed value is derived from a persisted monotonic + anchor at read time; the notification is the **platform chronometer counting + up**, with its base `now - elapsed` **derived at post time and never stored**, + which is how the run record keeps its "no wall-clock field whatsoever" rule; + its channel is `IMPORTANCE_LOW` and silent, because nothing here ever + finishes, so there is no alert-once rule and no session window anywhere in the + path; the readout is hundredths and it is **two strings**, so the + fastest-changing glyphs are not the loudest ones; best and worst need **three** + laps, ties go to the earliest, and all-equal means neither; laps are capped at + **999**, guarded in the engine over one `COUNT(*)` so appending never + materialises the rows; and the tab ticks at ~60 Hz **only while running**, so + a paused or idle stopwatch costs nothing. + + Three things the checklist did not name were changed anyway, and are recorded + in `ARCHITECTURE.md`: **the live pill's stopwatch actions now go through + `StopwatchEngine`** (M6 recorded this as knowingly open), with the pill's own + contract unchanged and `LivePillSelectorTest` passing **unmodified** as the + proof; **`LivePillSelector` was re-pointed at a new pure `StopwatchReadings`**, + the same extraction M6 made for timers, so the pill, the shade and the tab + cannot answer "what does this read right now" three different ways; and + **`ui/theme/Type.kt` stopped being `Typography()`** โ€” tabular figures on the + display, headline and title roles, app-wide, with no call-site change + anywhere, plus `ClockulaReadoutDefaults` naming the three roles a readout uses. + + The correctness find: a run that met a reboot mid-lap had `accumulated = 0` + while its laps still recorded forty seconds, so the honest reading is + **floored at the last lap's total** as well as at zero โ€” a readout sitting + below a lap the same run printed is not one anyone can believe, and the laps + are real measurements that are never deleted to tidy the arithmetic. Recorded + in `ARCHITECTURE.md` ยง5. The review found the other half of it: the floor has + to be **banked, not only displayed** โ€” resuming writes + `accumulated = max(accumulated, lastLapCumulative)`, and pausing and lapping + bank the same floored reading โ€” or the record ticks from below its own last + lap, the shade's free-running chronometer counts while the tab sits frozen, + and the next lap records a cumulative below the previous one. The same review + pass moved the engine's verbs onto **the reading's** mode rather than the + stored row's, so a stale record's Resume button resumes instead of hitting a + silent no-op. `pauseAfterReboot` itself is still untouched. + + No schema change (the database stays at v2; `LapDao` gains one `COUNT(*)` and + nothing else), **one** new permission โ€” `FOREGROUND_SERVICE_SPECIAL_USE`, the + one that names what the service actually does โ€” and **no floret-kit change**: + the kit stays at 0.6.0, untouched. + + Knowingly open: no lap export, sharing, deletion or editing, and no "previous + run" history; no Lap button on the live pill (it has two controls by design); + no user-configurable lap cap or readout precision; no reordering, grouping or + filtering of laps; no countdown-to-start, split-lap alarm, sound or vibration + from the stopwatch (a build rule enforces the silence); no analog stopwatch + face and no `MaterialShapes` showpiece, which `PLAN.md` ยง8 reserves for the + world-clock face, the ring screen and timer progress. `StopwatchReading.anchorIsStale` + is still not surfaced, the same answer M6 gave for timers. Of `AlarmClock`'s + intent contract only the in-process hook `ShellNavigation.tabForAction` grew a + second 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 and M6's four carry. + ### โฌœ 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