docs: mark M9 done
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 8s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Failing after 6m50s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Skipped

This commit is contained in:
2026-09-23 11:02:24 +02:00
parent 9ab3927efe
commit 3ff74dc1d5
+104 -15
View File
@@ -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<String, Any?>` 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 `<uses-permission>`**: 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.