From 3ff74dc1d53a81e26a7c255414107f17b79b3759 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 23 Sep 2026 11:02:24 +0200 Subject: [PATCH] docs: mark M9 done --- docs/ROADMAP.md | 119 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 15 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 56417d3..de4a801 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -10,20 +10,20 @@ Status legend: โœ… done ยท ๐Ÿšง in progress ยท โฌœ not started ## Current state (one line) -โœ… **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. +โœ… **M9 done.** Every app on the device โ€” an assistant, an automation app, a +watch companion, a shell script โ€” can now drive Clockula through +`android.provider.AlarmClock`, and no extra it sends can make Clockula write +something the user did not ask for. All **seven** of the contract's actions are +answered through **one** exported, permission-guarded, window-less door, behind +which everything that decides is pure Kotlin: an hour of 25 is dropped rather +than clamped to 23, a snooze of 1 000 minutes is clamped rather than refused, a +`file://` ringtone inherits the default instead of blocking the alarm, and an +intent hostile in every extra at once still leaves exactly one alarm and the +user looking at it. A voice-set alarm is **transient** โ€” the schema went to v3 +for it โ€” so "wake me at 6:30" every morning no longer leaves a graveyard of dead +06:30 rows. And the status-bar alarm icon finally opens the alarms list instead +of a ring screen for an alarm that is not ringing. **M10** (settings, JSON +backup and the self-check screen) is next. --- @@ -491,12 +491,101 @@ deliberate showpiece. 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 +### โœ… 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. + โœ… 1 420 JVM tests, 281 of them new, and **still no Robolectric** โ€” kept now + for a *boundary*, which is the last place a project usually gives in, because + "it is an `Intent`" sounds like it needs a device. It does not: the door is + split so that everything which decides is pure (`AlarmClockRequests`, + `AlarmMatching`, `IntentExtras`, `InteropText`, `InteropDeepLinks`) and the + Android half is two policy-free files โ€” a `Bundle` reader and an `Intent` + builder. `AlarmClockHandler` orchestrates with **no `android.*` import**, as + all three engines do, and is tested over both **real** engines through a new + `InteropHarness` โ€” deliberately *not* a composition of the existing harnesses, + since each builds a DataStore over the same file name and DataStore refuses a + second instance over a live one. Four new `ArchitectureRulesTest` rules and a + new `ManifestRulesTest`, because the manifest is where this milestone could be + silently wrong and none of it fails a compiler. The five new instrumentation + tests bring the compiled total to 45. + + `ACTION_DISMISS_TIMER` โ€” the seventh action, named neither in this checklist + nor in `PLAN.md` ยง6's table โ€” was built anyway: "the full contract" is the + milestone's own first sentence, and shipping six of seven and calling it full + would be the kind of quiet gap this loop exists to avoid. + + The decisions worth knowing: **one range rule settles every extra** โ€” a value + that decides when something will ring in the *future* is **dropped** out of + range, a value that modifies what the user is doing *now* is **clamped**, so + hour 25 never becomes an enabled 23:00 alarm while a 1 000-minute snooze + becomes the 60 minutes `ClockPrefs` would have allowed; **a `Bundle` is + modelled honestly** as `Map` with four *total* accessors, + because hostile input is an `Int` where a `String` was documented far more + often than it is an out-of-range number, and the platform's typed getters + cannot tell "absent" from "wrong type"; **`SKIP_UI` never means skip + validation** โ€” an incomplete `SET_ALARM` creates the row from the extras that + *were* valid, at the next whole hour, and lands the user in its editor, since + M5 locked that the editor edits a persisted row and nothing is ever written + and hidden; **`ALARM_SEARCH_MODE_TIME` matches exactly, never "nearest"**, and + an ambiguous 0..12 hour with no `IS_PM` carries *both* readings as candidates + so the question only reaches the user when the data cannot settle it; + **`ALARM_SEARCH_MODE_ALL` never asks** โ€” taken literally the contract's "show + the results" would make the mode useless โ€” while every other search with two + or more matches opens the milestone's one new surface, an M3 basic dialog with + a `ListItem` list where tapping a row *is* the answer. + + **A correctness find, fixed here:** the `AlarmClockInfo` **`showIntent`** โ€” the + thing the system opens when the user taps the status-bar alarm icon or the + lockscreen's next-alarm line โ€” pointed at the *ring screen* with + `CLEAR_TASK`, so tapping "my next alarm" opened a ring screen for an alarm + that was not ringing, which resolved to `Finished` and closed itself. It now + opens `MainActivity` on the Alarms tab, and a build rule fails the build if + `AndroidAlarmScheduler` ever names `AlarmRingActivity` again. That intent is + literally what "next-alarm publishing" means to a user. + + **The schema went to v3**: `delete_after_use` on `alarms` and on `timers`, two + `ALTER TABLE`s in `MIGRATION_2_3`, `3.json` exported and committed, `1.json` + and `2.json` untouched. It is not optional polish โ€” the contract says in two + places that a `SKIP_UI` alarm or timer should be removed once dismissed, and + without it a daily "wake me at 6:30" turns the Alarms tab into a graveyard. + The flag is set only on the `SKIP_UI` create path and honoured inside the two + engines, under their locks. + + Four things outside the checklist were changed anyway, and are recorded in + `ARCHITECTURE.md`: **`AlarmEngine` gained `dismissUpcoming(id)` and a + `minutesOverride` on `snooze`**, because both move the ring slot, the + auto-silence backstop and the next-alarm registration, and a handler calling + three repositories in a row would race a fire broadcast; **`TimerEngine` gained + `dismissAllExpired()` and `dismissExpired(id)`** โ€” the first is the "stop all" + the UI still deliberately does not offer, because that rule was about a thumb + on a ring and a programmatic request has stated exactly what it is discarding; + **`domain/text/TextFolding` was extracted** and `ZoneSearch` delegates to it, + with `ZoneSearchTest` passing unmodified as the proof; and **the shell's + `openTab` became `openRequest: ShellRequest?`**, since three of the contract's + outcomes are not "select a tab". + + **No new ``**: the one new permission string is + `android:permission` *on the door*, a requirement on the caller. **No new + dependency coordinate** and **no floret-kit change** โ€” the kit stays at 0.6.0, + untouched, because the one new surface is material3's own `AlertDialog`. + + Knowingly open: no voice-interaction follow-on flows + (`VoiceInteractor.CompleteVoiceRequest`/`PickOptionRequest`), so + `android.intent.category.VOICE` is deliberately not declared and Clockula + accepts a `clockula://` deeplink while publishing none; no "most closely + matched" time search; no toast on a `SKIP_UI` write โ€” the status-bar icon is + the platform's own receipt; no `delete_after_use` surface in either editor, so + an assistant-set alarm the user edits stays transient, as it does in Google + Clock; no `EXTRA_MESSAGE` carried into the timer setup panel when no length + was given; and `AlarmScheduler.systemNextAlarm()` is still caller-less โ€” it + belongs to M10's self-check screen. The five new instrumentation tests and the + two new migration cases compile in the gate but have **never run on + hardware**, as no device was attached โ€” the same caveat M5's seven, M6's four, + M7's four and M8's four carry. + ### โฌœ M10 โ€” Settings, backup, and the self-check - Settings composed from kit components: theme, dynamic colour, language, alarm/timer/clock defaults, about + crash reporting.