diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1d0c0c9..56417d3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,18 +10,20 @@ Status legend: βœ… done Β· 🚧 in progress Β· ⬜ not started ## Current state (one line) -βœ… **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. +βœ… **M8 done.** The World clock tab is real, and **every tab now has its own +content** β€” no empty state is left anywhere in the app. A hero analog face shows +your home zone, and its dial morphs between a circle and a sun with the hour +there, so a glance answers "is it the middle of the night" without a line of +text saying so β€” the app's one deliberate `MaterialShapes` showpiece, spent on +information rather than decoration. Cities come from the device's own tzdata, +named and localised through ICU behind a seam, searched by word-prefix over +folded text so ASCII finds SΓ£o Paulo; each row carries its time, its offset and +its day difference **relative to home**, all read at the instant so both sides +of a daylight-saving change are right on the day. The list reorders by drag or +by a move-up/move-down action a screen reader can reach, and the home zone is +set by hand or left following the device. **M9** (system interop β€” the full +`android.provider.AlarmClock` contract and its hostile-input validation) is +next. --- @@ -411,12 +413,84 @@ M7 owns only how the paused result is presented. 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 +### βœ… 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. + βœ… 1 139 JVM tests, 171 of them new, and **still no Robolectric** β€” kept now + for a fourth screen, and this was the milestone most tempted to break it, + because ICU and `Canvas` both look like they need a device. They do not: + everything ICU answers is behind `ZoneNames` (faked, with per-method call + counters and a `failEverything` flag), everything the face draws is + `DrawScope` calls over `AnalogFace`'s angles and `DayNight`'s fraction β€” two + pure objects asserted at every minute of the day without a pixel β€” and + everything the list and the picker decide is `WorldClockRows`, + `ZonePickerRows`, `ZoneSearch`, `ZoneComparisons`, `ZoneCatalog` and + `WorldClocks`. Every write is a `WorldClockViewModel` method over the **real** + `WorldClockRepositoryImpl` across `FakeWorldClockDao` (so `addIfAbsent` and + `reorder`'s real transactions run) and a real DataStore under a `@TempDir`. + Three new `ArchitectureRulesTest` rules; the four new instrumentation tests + bring the compiled total to 40. + + The decisions worth knowing: **there is no engine and must not be one** β€” a + world clock owns no AlarmManager slot, no service and no ring session, so a + fourth engine would be three empty seams and a `Mutex` guarding nothing; the + tab reads the **wall clock**, and re-reads both the instant and the device + zone on **every tick**, which is what makes `TIME_SET` and `TIMEZONE_CHANGED` + free (no new receiver branch); the zone **ids** stay pure (through a new + `ZoneProvider.available()`) while only the **names** cross the ICU seam, which + is one file, `runCatching`-guarded end to end and pinned there by a build + rule; the catalog keeps only region-based ids that are **their own canonical + id** β€” so the tzdata's own canonicalisation drops Asia/Calcutta in favour of + Asia/Kolkata and no table of ours can rot β€” unless the canonical id is not on + the device at all, since ICU and tzdata are separately updatable APEX modules + that can disagree, plus `UTC` by name; search is + **word-prefix over folded text**, so "erl" deliberately does not find Berlin; + the offset and day are computed **at the instant** from both zones' rules, the + same discipline `AlarmOccurrences` uses, so Berlin↔Sydney is ten hours in + January and eight in July; labels are **data, not strings**, so Kathmandu is + `Ahead(4, 45)` rather than a signed 285 for a translator to interpret; the + cadence is **200 ms** with the state truncated to the second (face) and the + minute (rows) and `distinctUntilChanged` over it; the list is capped at + **24**, with the cap spelled out on the screen once it is reached, because + M3's `ExtendedFloatingActionButton` has no `enabled` to dim; and + reduce-motion snaps the morph but **never stops the second hand**, + because a clock whose second hand stops is a broken clock. + + Three things outside the checklist were changed anyway, and are recorded in + `ARCHITECTURE.md`: **`ZoneProvider` gained `available()`**, because + `ZoneId.getAvailableZoneIds()` at a call site is the same ambient read + `PLAN.md` Β§5 bans for `now()`; **`ui/common/AlarmTimeFormatter.kt` gained + `rememberLocalTimeFormatter`** with the existing function delegating to it, so + M5's "one 12/24-hour formatter" survives a fourth caller instead of growing a + second answer; and **`ShellDefaults.EmptyTabScreen` was deleted**, its own + KDoc having said it existed only until each of M5–M8 replaced its own tab β€” + the world clock was its last caller. + + No schema change at all (the database stays at v2; `WorldClockEntity`, its + DAO, its mapper and its repository are not edited), **no new permission and no + manifest entry**, **one** new dependency coordinate β€” + `androidx.graphics:graphics-shapes:1.0.1`, pinned to the version that already + resolved transitively, because M8 imports that package directly β€” and **no + floret-kit change**: the kit stays at 0.6.0, untouched, with + `ReorderableColumn`, `GroupedSurface`, `FullScreenPicker`, `InlineTextField` + and `CollapsingScaffold`'s FAB slot already covering the screen. + + Knowingly open: no rename UI for `world_clocks.label` β€” the column is honoured + on read and M10's backup import will carry labels; no automatic "home" row + while travelling (a separate feature with its own preference, and the hero + face already follows the device); no per-city detail screen, map, + sunrise/sunset or day-length bar β€” `DayNight` is a stated convention for a + shape morph, not an ephemeris; no live times or long zone names in the picker; + no seeded data, so a fresh install really has an empty list; no reordering by + anything but `sort_order`; and no `MaterialShapes` on the ring screen or timer + progress, which `PLAN.md` Β§8 also sanctions β€” the build rule makes widening + that allowlist a deliberate edit. 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, M6's four and M7's four carry. + ### ⬜ 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