docs: plain-language pass over every doc
Removes AI-writing patterns across the docs, store listing and templates: dash-heavy rhythm, not-X-but-Y contrasts, one-line kickers and decorative bold. Facts, code, links and headings are unchanged. Also fixes leftovers copied from the sibling apps: the crash report template promised "no calendar content", and CONTRIBUTING described Agendula's task-provider layout, test seams and commit style.
This commit is contained in:
1 parent
881f6b1286
commit
e870918a03
11 files changed
+697
-697
No files matched your search
+393
-395
File diff suppressed because it is too large.
Load diff
+14
-14
@@ -1,6 +1,6 @@
|
||||
# Clockula backup format
|
||||
|
||||
`clockula-backup-v1.json` — a documented, versioned JSON export of Clockula's
|
||||
`clockula-backup-v1.json` is a documented, versioned JSON export of Clockula's
|
||||
own data, written to (and read from) a user-chosen folder via SAF
|
||||
(`docs/PLAN.md` §7). This file is the schema's source of truth; the codec
|
||||
(`data/backup/BackupDto.kt`, `BackupCodec.kt`) implements exactly what is
|
||||
@@ -20,18 +20,18 @@ written here.
|
||||
}
|
||||
```
|
||||
|
||||
- `format` — must be exactly `"clockula-backup"`.
|
||||
- `version` — must be exactly `1`. Any other value rejects the whole file:
|
||||
- `format`: must be exactly `"clockula-backup"`.
|
||||
- `version`: must be exactly `1`. Any other value rejects the whole file:
|
||||
Clockula does not guess at a schema it has not implemented.
|
||||
- `exportedAt` — epoch milliseconds, informational only (not re-imported).
|
||||
- `exportedAt`: epoch milliseconds, informational only (not re-imported).
|
||||
|
||||
## Parsing rules
|
||||
|
||||
- **Tolerant of unknown fields, at any level.** An older or newer client's
|
||||
extra field is ignored, never an error.
|
||||
- **Strict about known fields.** A malformed value in a field this schema
|
||||
defines — `format`, `version`, an alarm's `hour`/`minute`, a world clock's
|
||||
`zone_id`, a timer's `duration_millis` — rejects the **whole file** with a
|
||||
defines (`format`, `version`, an alarm's `hour`/`minute`, a world clock's
|
||||
`zone_id`, a timer's `duration_millis`) rejects the **whole file** with a
|
||||
specific reason. Nothing is imported half-way.
|
||||
- Settings values (snooze minutes, volume ramp, …) are
|
||||
**not** range-checked at parse time; they pass through the same clamps
|
||||
@@ -41,19 +41,19 @@ written here.
|
||||
- An enum field with a name this version does not recognise (a `dismiss_
|
||||
challenge` or `theme_mode` from a future version, say) degrades to that
|
||||
field's default, the same forgiving-read posture `AlarmMapper` already uses
|
||||
for a corrupt Room row — it does not fail the import.
|
||||
for a corrupt Room row. It does not fail the import.
|
||||
|
||||
## Import semantics
|
||||
|
||||
Import **replaces all state** — alarms, timers and world clocks are deleted
|
||||
Import **replaces all state**: alarms, timers and world clocks are deleted
|
||||
and re-created from the file, and every setting the file carries is written
|
||||
over the current value. This is not a merge, and the backup screen says so
|
||||
before the user confirms. A ringing alarm is dismissed and any running timer
|
||||
is deleted *before* the replace, so the import never leaves an orphaned ring
|
||||
session; alarm/timer/stopwatch scheduling is re-resolved immediately after.
|
||||
|
||||
Every imported alarm and timer gets a **fresh id and fresh timestamps** —
|
||||
the backup carries no id and no `created_at`/`updated_at`, since those are
|
||||
Every imported alarm and timer gets a **fresh id and fresh timestamps**.
|
||||
The backup carries no id and no `created_at`/`updated_at`, since those are
|
||||
local-database facts, not portable ones.
|
||||
|
||||
## Alarms
|
||||
@@ -77,7 +77,7 @@ One object per row of `alarms`, config only:
|
||||
| `delete_after_use` | Boolean | A transient, contract-created alarm. |
|
||||
|
||||
**Not exported:** the `alarm_states` ring-state table (snooze/ringing/handled-
|
||||
occurrence bookkeeping) — it is volatile, per-device state, not configuration,
|
||||
occurrence bookkeeping). It is volatile, per-device state, not configuration,
|
||||
and `docs/ARCHITECTURE.md` §4/§6 are explicit that it must never appear here.
|
||||
|
||||
## Timers
|
||||
@@ -93,7 +93,7 @@ One object per row of `timers`, config only:
|
||||
| `delete_after_use` | Boolean | |
|
||||
|
||||
**Not exported:** `state`, `remaining`, the elapsed-realtime anchors, or
|
||||
`ends_at_wall_clock` — a timer's running state is meaningless on another
|
||||
`ends_at_wall_clock`. A timer's running state is meaningless on another
|
||||
device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
|
||||
`IDLE`, with `remaining` equal to `duration`.
|
||||
|
||||
@@ -128,12 +128,12 @@ files may also carry `default_timer_duration_millis`, from a default timer
|
||||
length setting that has since been removed; it is ignored on import like any
|
||||
other unknown field. Every
|
||||
numeric/duration value is clamped on *apply*, through the existing pref
|
||||
setters — never on parse.
|
||||
setters, never on parse.
|
||||
|
||||
## Ringtone URIs
|
||||
|
||||
Exported exactly as stored (a `content://` URI, typically). On import the
|
||||
URI is written back unchanged; if the *importing* device cannot resolve it
|
||||
at ring/preview time, the existing ringtone-resolution fallback already used
|
||||
elsewhere in the app applies (falls through to the device default) — this is
|
||||
elsewhere in the app applies (falls through to the device default). This is
|
||||
not special-cased in the backup path itself.
|
||||
+34
-34
@@ -1,6 +1,6 @@
|
||||
# Clockula — implementation plan
|
||||
|
||||
> The **design decisions and their rationale** — the "why". Locked unless
|
||||
> The design decisions and their rationale (the "why"). Locked unless
|
||||
> explicitly overturned here. For the moving status view see
|
||||
> [`ROADMAP.md`](ROADMAP.md); for the shape of the code as it actually stands,
|
||||
> [`ARCHITECTURE.md`](ARCHITECTURE.md) (written from M2 onward).
|
||||
@@ -32,7 +32,7 @@ in three concrete places:
|
||||
1. **The platform alarm contract.** Clockula implements `android.provider.AlarmClock`
|
||||
in full (§6), so Assistant, automation apps and anything else that speaks the
|
||||
standard intent vocabulary can drive it. That contract *is* the open interface
|
||||
for clocks on Android — the same role `CalendarContract` plays for Calendula.
|
||||
for clocks on Android, the same role `CalendarContract` plays for Calendula.
|
||||
Being a good citizen of it is the thesis, applied.
|
||||
2. **IANA tzdata.** World clocks are IANA zone IDs, not a bespoke city table (§5).
|
||||
3. **Your data leaves whenever you want.** A documented, versioned JSON backup
|
||||
@@ -44,7 +44,7 @@ Clockula owns storage, but **nothing above the data layer knows Room exists**.
|
||||
The app talks to `AlarmRepository` / `TimerRepository` / `WorldClockRepository` /
|
||||
`StopwatchRepository`; only the DAOs and one data source per aggregate touch Room
|
||||
entities. Domain models are plain Kotlin. **Never let a Room entity or a
|
||||
`@Query` string leak above the data layer** — the same discipline that let
|
||||
`@Query` string leak above the data layer**. That is the same discipline that let
|
||||
Agendula swap its whole storage posture without touching the UI.
|
||||
|
||||
---
|
||||
@@ -89,7 +89,7 @@ recurrence) are a different animal from a due-date nudge.
|
||||
|
||||
## 3. floret-kit
|
||||
|
||||
Consumed as a git submodule + Gradle composite build, as the siblings do —
|
||||
Consumed as a git submodule + Gradle composite build, as the siblings do:
|
||||
`includeBuild("floret-kit")`, `implementation("de.jeanlucmakiola.floret:<module>")`,
|
||||
with a gitignored `clockula/floret-kit/local.properties` locally and
|
||||
`ANDROID_HOME` in CI.
|
||||
@@ -128,7 +128,7 @@ Everything else in this app is a readout. This is the engineering.
|
||||
`TIME_SET`, `TIMEZONE_CHANGED`, and `MY_PACKAGE_REPLACED`. DST transitions are
|
||||
handled by this re-resolution, not by arithmetic on stored millis. A 02:30
|
||||
alarm on a spring-forward night has a defined, tested behaviour.
|
||||
- Scheduled with `AlarmManager.setAlarmClock()` — not `setExactAndAllowWhileIdle`.
|
||||
- Scheduled with `AlarmManager.setAlarmClock()`, not `setExactAndAllowWhileIdle`.
|
||||
`setAlarmClock` is the only variant the system treats as a user-visible alarm:
|
||||
it is doze-exempt, and it populates `getNextAlarmClock()`, which is what draws
|
||||
the status-bar alarm icon and the lockscreen line. Permission is
|
||||
@@ -147,7 +147,7 @@ notification whose content intent is the ring activity.
|
||||
- Full-screen intent on Android 14+ needs `USE_FULL_SCREEN_INTENT`; check
|
||||
`canUseFullScreenIntent()` and deep-link to the grant screen when denied. A
|
||||
denied FSI must degrade to a high-priority heads-up notification that still
|
||||
rings — never to silence.
|
||||
rings, never to silence.
|
||||
- Gradual volume ramp, per-alarm ringtone (system `RingtoneManager` URIs, so the
|
||||
user's existing sounds work), vibration pattern, `AudioAttributes` with
|
||||
`USAGE_ALARM` so DND's alarm exemption applies.
|
||||
@@ -162,7 +162,7 @@ notification whose content intent is the ring activity.
|
||||
A "**Why might my alarm not ring?**" screen that reports actual device state, not
|
||||
generic advice: exact-alarm permission, notification permission, full-screen-intent
|
||||
permission, battery-optimisation exemption, **whether DND currently blocks alarms**
|
||||
(read via `NotificationManager.getCurrentInterruptionFilter()` — Clockula never
|
||||
(read via `NotificationManager.getCurrentInterruptionFilter()`; Clockula never
|
||||
requests `ACCESS_NOTIFICATION_POLICY` or changes DND itself, it only reports what the
|
||||
user already has), whether the OEM has Clockula under an aggressive app-killer, and
|
||||
the next scheduled fire time as the system sees it (`getNextAlarmClock()`), compared
|
||||
@@ -175,19 +175,19 @@ the exact settings page. This screen is a v1 feature, not a support afterthought
|
||||
|
||||
Room, schema exported and version-controlled, migrations tested from v1.
|
||||
|
||||
- **`alarms`** — id, hour, minute, label, enabled, repeat-day mask, ringtone URI,
|
||||
- **`alarms`**: id, hour, minute, label, enabled, repeat-day mask, ringtone URI,
|
||||
vibrate, snooze minutes, snooze limit, volume-ramp seconds, dismiss challenge,
|
||||
`skipNextOccurrence`, timestamps.
|
||||
- **`timers`** — id, label, duration, state (idle/running/paused/expired), the
|
||||
- **`timers`**: id, label, duration, state (idle/running/paused/expired), the
|
||||
elapsed-realtime anchor it resolves against, ringtone, sort order. Multiple
|
||||
concurrent timers are first-class.
|
||||
- **`world_clocks`** — id, IANA zone ID, optional custom label, sort order.
|
||||
- **`stopwatch_laps`** — id, index, split, cumulative. The stopwatch's running
|
||||
- **`world_clocks`**: id, IANA zone ID, optional custom label, sort order.
|
||||
- **`stopwatch_laps`**: id, index, split, cumulative. The stopwatch's running
|
||||
state lives in DataStore (a single record), its laps in Room so they survive
|
||||
process death.
|
||||
|
||||
Running timers and the stopwatch anchor on **`SystemClock.elapsedRealtime()`**,
|
||||
never wall-clock — a user changing the time must not warp a running timer. Alarms
|
||||
never wall-clock: a user changing the time must not warp a running timer. Alarms
|
||||
are the opposite: wall-clock by definition. This distinction is the single
|
||||
easiest thing to get wrong in a clock app; it is tested both ways.
|
||||
|
||||
@@ -226,12 +226,12 @@ than writing anything. `SKIP_UI` never means "skip validation".
|
||||
"timers": […], "worldClocks": […], "settings": {…} }`. The schema is **documented
|
||||
in the repo** (`docs/BACKUP.md`), versioned, and import is tolerant of unknown
|
||||
fields (ignored, at any level) and strict about known ones (a malformed known
|
||||
field rejects the whole file — nothing is imported half-way). Ringtone URIs are
|
||||
field rejects the whole file, so nothing is imported half-way). Ringtone URIs are
|
||||
exported as-is with a documented fallback when the target device can't resolve
|
||||
them.
|
||||
|
||||
Import is **whole-state replacement**, not a merge: the user is shown exactly
|
||||
that before confirming. Alarm and timer records carry only configuration — ring
|
||||
that before confirming. Alarm and timer records carry only configuration. Ring
|
||||
state (`alarm_states`, a timer's running anchors) is never exported and never
|
||||
touched by an import; an imported timer always lands `IDLE`. Record field names
|
||||
mirror the Room column vocabulary (§5), so the schema, the exported Room schema
|
||||
@@ -241,7 +241,7 @@ and the backup all read the same words.
|
||||
|
||||
## 8. Identity
|
||||
|
||||
Seed **`#6B7A5C`** — a muted sage/olive.
|
||||
Seed **`#6B7A5C`**, a muted sage/olive.
|
||||
|
||||
The family's seeds are one colour rotated: Calendula `#5C6B7A` (slate blue) →
|
||||
Agendula `#7A5C6B` (warm mauve) → Clockula `#6B7A5C` (sage). Same three bytes,
|
||||
@@ -254,10 +254,10 @@ fallback, hand-tuned light/dark `ColorScheme`s in `ui/theme/Color.kt`.
|
||||
|
||||
Design principles inherited from the family (non-negotiable):
|
||||
- **Expressive ≠ big or bold.** Refinement comes from shape, colour, space and
|
||||
motion — never from enlarging text or upping weight.
|
||||
motion, never from enlarging text or upping weight.
|
||||
- Content lives **on surfaces**, not as bare text on the background.
|
||||
- `MaterialShapes` / `androidx.graphics.shapes` morphs are for the genuinely
|
||||
clock-specific surfaces — the analog face, the ring screen, timer progress —
|
||||
clock-specific surfaces (the analog face, the ring screen, timer progress),
|
||||
not sprinkled everywhere.
|
||||
|
||||
A clock app is mostly one enormous number. That readout is where the design
|
||||
@@ -267,24 +267,24 @@ lives, and it is the thing to get right before anything else.
|
||||
|
||||
## 9. Navigation
|
||||
|
||||
Four tabs — Alarm, Timer, Stopwatch, World clock — in an M3 navigation bar, with
|
||||
Four tabs (Alarm, Timer, Stopwatch, World clock) in an M3 navigation bar, with
|
||||
a **live pill** floating above it whenever a timer or the stopwatch is running:
|
||||
current value plus pause and stop, reachable from any tab.
|
||||
|
||||
This is the one deliberate departure from the stock model, and it exists because
|
||||
the stock clock's worst flaw is that a running timer is invisible unless you are
|
||||
standing on its tab. The pill is not decoration; it is the fix.
|
||||
standing on its tab.
|
||||
|
||||
Adaptive: navigation rail on wide layouts, per M3 adaptive guidance.
|
||||
|
||||
### Home-screen widgets (M10)
|
||||
|
||||
Three Jetpack Glance app widgets, each a read-only mirror of state that already
|
||||
exists — no widget owns data: **next alarm** (day, time, label), **timer**
|
||||
exists (no widget owns data): **next alarm** (day, time, label), **timer**
|
||||
(the live pill's subject timer, same selection logic, with pause/resume/reset),
|
||||
and **clock** (local time, growing to world clocks at larger sizes). Tapping a
|
||||
widget opens the matching tab through the same `MainActivity` intents the
|
||||
`AlarmClock` contract (§6) already uses — a widget is just another caller of
|
||||
`AlarmClock` contract (§6) already uses. A widget is just another caller of
|
||||
that door, not a second entry point. Refresh is event-driven (`updatePeriodMillis
|
||||
= 0`): widgets update when the state they mirror changes, not on a timer.
|
||||
|
||||
@@ -292,24 +292,24 @@ that door, not a second entry point. Refresh is event-driven (`updatePeriodMilli
|
||||
|
||||
## 10. Repository & pipeline
|
||||
|
||||
**Codeberg is canonical from the first commit** — git, issues, PRs and releases.
|
||||
**Codeberg is canonical from the first commit**: git, issues, PRs and releases.
|
||||
The self-hosted Gitea instance holds the secrets and runs the release job, and
|
||||
push-mirrors *from* Codeberg. There is no Gitea-canonical phase to migrate out
|
||||
of, which is the mistake Agendula had to unwind.
|
||||
|
||||
The pipeline is copied wholesale from the siblings:
|
||||
|
||||
- `.forgejo/workflows/ci.yaml` — contributor-facing, no secrets. Copy Agendula's,
|
||||
- `.forgejo/workflows/ci.yaml`: contributor-facing, no secrets. Copy Agendula's,
|
||||
adjusting `SKIP_RE`.
|
||||
- `.forgejo/workflows/translations.yaml` — identical in both siblings; copy as-is.
|
||||
- `.gitea/workflows/release.yaml` — **Agendula's spine** (Codeberg-canonical: pushes
|
||||
- `.forgejo/workflows/translations.yaml`: identical in both siblings; copy as-is.
|
||||
- `.gitea/workflows/release.yaml`: **Agendula's spine** (Codeberg-canonical: pushes
|
||||
the tag to Codeberg itself, pre-1.0 prerelease flag, and the Codeberg publish
|
||||
step is *not* `continue-on-error`, after five Calendula-era releases reported
|
||||
green while never publishing) **plus Calendula's `play` job** grafted on
|
||||
unchanged — last, isolated, `continue-on-error`, skipping cleanly until
|
||||
unchanged. It runs last, isolated and `continue-on-error`, and skips cleanly until
|
||||
`PLAY_SERVICE_ACCOUNT_JSON` exists. It therefore stays dormant through the
|
||||
pre-1.0 run, which is the correct behaviour anyway.
|
||||
- `.gitea/workflows/renovate.yml` — Agendula's, including the
|
||||
- `.gitea/workflows/renovate.yml`: Agendula's, including the
|
||||
`github.repository_owner == 'makiolaj'` guard.
|
||||
- Issue templates, PR template, F-Droid metadata, fastlane tree.
|
||||
|
||||
@@ -331,8 +331,8 @@ useful reference for the parts of an alarm app that are tedious to rediscover.
|
||||
- **Default posture: read it, don't copy it.** Understanding how it schedules and
|
||||
how its services are wired is free; retyping our own implementation from that
|
||||
understanding keeps Clockula cleanly MIT.
|
||||
- **Where we genuinely lift code** — realistically only intricate scheduling or
|
||||
service-lifecycle edge cases — that file keeps its Apache-2.0 header and
|
||||
- **Where we genuinely lift code** (realistically only intricate scheduling or
|
||||
service-lifecycle edge cases), that file keeps its Apache-2.0 header and
|
||||
attribution, is listed in `docs/PROVENANCE.md`, and the Apache text ships in
|
||||
`licenses/`. This is exactly the pattern Agendula already uses for the vendored
|
||||
dmfs provider, so it is a known, working arrangement rather than a new risk.
|
||||
@@ -348,16 +348,16 @@ DAO/repository, and the Room entity shapes as a sanity check on §5.
|
||||
- Its **always-on foreground service** (`AlarmAlwaysForegroundService`). Keeping a
|
||||
permanent service alive to make alarms reliable is a battery-cost workaround;
|
||||
`setAlarmClock` is the supported mechanism and does not need it.
|
||||
- Its **Pomodoro** feature — out of scope.
|
||||
- Its **Pomodoro** feature, which is out of scope.
|
||||
- Its **settings/tile component vocabulary** (`ui/components/tiles/*`,
|
||||
`SettingTile`) — floret-kit's `GroupedRow` / `OptionCard` / `OptionPicker` are
|
||||
`SettingTile`). floret-kit's `GroupedRow` / `OptionCard` / `OptionPicker` are
|
||||
our equivalents and keep the family coherent.
|
||||
- **MaterialKolor + a colour-picker dependency** — the kit's `identity` module
|
||||
- **MaterialKolor + a colour-picker dependency**. The kit's `identity` module
|
||||
owns theming.
|
||||
- Its **no-Hilt, manual-ViewModel structure** and `minSdk 24`.
|
||||
|
||||
For interaction detail — snooze behaviour, the ring screen, timer presentation,
|
||||
what the stopwatch does with laps — **Google Clock is the reference**, not
|
||||
For interaction detail (snooze behaviour, the ring screen, timer presentation,
|
||||
what the stopwatch does with laps), **Google Clock is the reference**, not
|
||||
ClockMaster.
|
||||
|
||||
---
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
# Clockula — documentation
|
||||
|
||||
Clockula is a Material 3 Expressive **clock** app for Android — alarms, timers,
|
||||
Clockula is a Material 3 Expressive **clock** app for Android: alarms, timers,
|
||||
stopwatch and world clock. Third floret of the bloom, after
|
||||
[Calendula](https://codeberg.org/jlmakiola/calendula) (calendar) and
|
||||
[Agendula](https://codeberg.org/jlmakiola/agendula) (tasks). See the top-level
|
||||
[`../README.md`](../README.md) for the pitch.
|
||||
|
||||
Unlike its siblings, Clockula owns its storage — there is no open provider behind
|
||||
Unlike its siblings, Clockula owns its storage, because there is no open provider behind
|
||||
a clock. What that costs and how the open-standards commitment is honoured anyway
|
||||
is the first section of [`PLAN.md`](PLAN.md).
|
||||
|
||||
@@ -15,8 +15,8 @@ is the first section of [`PLAN.md`](PLAN.md).
|
||||
| Doc | What it covers |
|
||||
|---|---|
|
||||
| [`PLAN.md`](PLAN.md) | **The spec.** Thesis, locked decisions, the alarm engine, data model, system interop, identity, pipeline, and how ClockMaster is used. The "why". |
|
||||
| [`ROADMAP.md`](ROADMAP.md) | **Status and the work queue** — milestones M0–M11 and post-v1. What the build loop works through. |
|
||||
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How Clockula is built **today** — layers, the data seam, the alarm engine, DI, build/tooling, manifest. Written from M2 onward. |
|
||||
| [`ROADMAP.md`](ROADMAP.md) | **Status and the work queue**: milestones M0–M11 and post-v1. What the build loop works through. |
|
||||
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How Clockula is built **today**: layers, the data seam, the alarm engine, DI, build/tooling, manifest. Written from M2 onward. |
|
||||
| [`RELEASING.md`](RELEASING.md) | How to cut a release. Written at M0 alongside the pipeline. |
|
||||
| [`PROVENANCE.md`](PROVENANCE.md) | Any third-party code carried in-tree and under what licence. Created only if we actually lift something (`PLAN.md` §11). |
|
||||
|
||||
|
||||
+207
-207
@@ -1,6 +1,6 @@
|
||||
# Clockula — roadmap
|
||||
|
||||
The **status** view, and the work queue. Design rationale for every decision here
|
||||
The status view and the work queue. Design rationale for every decision here
|
||||
lives in [`PLAN.md`](PLAN.md); the shape of the code as it stands will live in
|
||||
[`ARCHITECTURE.md`](ARCHITECTURE.md) from M2 onward.
|
||||
|
||||
@@ -13,16 +13,16 @@ Status legend: ✅ done · 🚧 in progress · ⬜ not started
|
||||
✅ **M10 done.** Clockula now has a settings hub (theme, dynamic colour,
|
||||
language, the alarm/timer/clock defaults every new alarm or timer inherits,
|
||||
help), a documented, versioned JSON backup (`docs/BACKUP.md`) that exports and
|
||||
restores alarms/timers/world clocks and settings through SAF — configuration
|
||||
only, never ring state, never a timer's running anchors — as a whole-state
|
||||
restores alarms/timers/world clocks and settings through SAF (configuration
|
||||
only, never ring state, never a timer's running anchors) as a whole-state
|
||||
replace the screen is explicit about before the user confirms, a "why might my
|
||||
alarm not ring?" self-check that reads real device state (exact-alarm and
|
||||
notification permissions, full-screen intent, battery optimisation, DND, alarm
|
||||
volume, a known aggressive-OEM allowlist, and the system's own next alarm
|
||||
compared against what Clockula computed) and deep-links to the exact settings
|
||||
page for each, and three Glance home-screen widgets — next alarm, timer,
|
||||
clock — that mirror existing state and redraw event-driven, on every write,
|
||||
with no ticker of their own. **M11** (release readiness) is next.
|
||||
page for each, and three Glance home-screen widgets (next alarm, timer,
|
||||
clock) that mirror existing state and redraw event-driven, on every write,
|
||||
with no ticker of their own. M11 (release readiness) is next.
|
||||
|
||||
---
|
||||
|
||||
@@ -30,12 +30,12 @@ with no ticker of their own. **M11** (release readiness) is next.
|
||||
|
||||
One milestone per iteration, auto-continuing. Each iteration:
|
||||
|
||||
1. Pick the **first milestone not marked ✅**.
|
||||
2. Implement it **completely** — every checklist item under it.
|
||||
1. Pick the first milestone not marked ✅.
|
||||
2. Implement it completely: every checklist item under it.
|
||||
3. Build, and run the tests the milestone names. A milestone is not done until
|
||||
they pass.
|
||||
4. **Commit** (floret-kit submodule first where it changed, then Clockula's
|
||||
pointer bump — separate commits, granular, lowercase area prefix).
|
||||
pointer bump; separate commits, granular, lowercase area prefix).
|
||||
5. Mark the milestone ✅ here, update **Current state**, and commit that too.
|
||||
6. Continue to the next milestone.
|
||||
|
||||
@@ -45,7 +45,7 @@ Rules that hold across every iteration:
|
||||
(`PLAN.md` §0).
|
||||
- **Never** add a foojay toolchain resolver, anywhere, not even in a comment.
|
||||
- Commit each finished unit of work; never accumulate a WIP pile.
|
||||
- If a milestone turns out to be wrong, say so and amend `PLAN.md` — do not
|
||||
- If a milestone turns out to be wrong, say so and amend `PLAN.md`. Do not
|
||||
silently build something else.
|
||||
- `ARCHITECTURE.md` and `CHANGELOG.md` are updated as part of the milestone that
|
||||
changes them, not in a later cleanup pass.
|
||||
@@ -75,7 +75,7 @@ The project exists, builds, is themed, and ships itself.
|
||||
`scripts/check_reproducible_release.sh` reports every invariant holding.
|
||||
Two notes for later: the store listing and `fastlane` changelogs are written
|
||||
but no screenshots exist yet (M11), and `docs/ARCHITECTURE.md` is still
|
||||
unwritten by design — M2 writes it, once there is an architecture to
|
||||
unwritten by design. M2 writes it, once there is an architecture to
|
||||
describe.
|
||||
|
||||
### ✅ M1 — floret-kit: `core-prefs` + `core-di`
|
||||
@@ -83,16 +83,16 @@ Pay the extraction the kit's roadmap has been waiting for a third app to trigger
|
||||
- In the kit: `core-prefs` (`ThemeMode`, `dynamicColor`, typed DataStore wrapper,
|
||||
`toEnum()`) and `core-di` (`@IoDispatcher` + provider), each with tests, module
|
||||
docs, `CHANGELOG` and `ROADMAP`/`ARCHITECTURE` updates.
|
||||
- Clockula consumes both. Migrating Calendula and Agendula is explicitly **out of
|
||||
scope** — note it in the kit's roadmap as a follow-up.
|
||||
- Clockula consumes both. Migrating Calendula and Agendula is explicitly out of
|
||||
scope; note it in the kit's roadmap as a follow-up.
|
||||
- **Done when:** the kit's tests pass, Clockula builds against the new modules,
|
||||
and both repos are committed (submodule first, then the pointer bump).
|
||||
|
||||
✅ floret-kit 0.4.0. `core-prefs` (56 kit tests) gives the family a typed
|
||||
`Pref<T>`/`PrefStore` over DataStore; reads degrade to the default instead of
|
||||
throwing, and `flow()` is distinct-until-changed. The appearance key names are
|
||||
frozen at what Calendula and Agendula already write, so the sibling migration —
|
||||
a follow-up in the kit's roadmap, not here — needs no data migration.
|
||||
frozen at what Calendula and Agendula already write, so the sibling migration
|
||||
(a follow-up in the kit's roadmap, not here) needs no data migration.
|
||||
`core-di` carries `hilt-compiler` itself, since an `@InstallIn` module only
|
||||
reaches a consumer's component if its own library was processed; Clockula's
|
||||
`assembleDebug` is the integration test for that binding. Clockula's theme now
|
||||
@@ -114,19 +114,19 @@ The whole storage stack, headless. No UI.
|
||||
|
||||
✅ Four tables (`alarms`, `timers`, `world_clocks`, `stopwatch_laps`), schema
|
||||
v1 exported to `app/schemas` so migrations can be tested from the beginning.
|
||||
The seam that matters: the app talks to four repository interfaces speaking
|
||||
domain types and Flows, and `ArchitectureRulesTest` fails the build if Room, an
|
||||
entity or a query string ever appears above `data/`. Time is a parameter rather
|
||||
than an ambient read — `WallClock` and `ElapsedRealtimeClock` are distinct
|
||||
types, so a running timer anchors on the monotonic clock and keeps a wall-clock
|
||||
value only as a post-reboot fallback, and the stopwatch keeps none at all. That
|
||||
leaves one honest gap, written down rather than glossed: once a new boot's
|
||||
uptime climbs past a stored anchor the reboot goes unnoticed, so closing it
|
||||
needs a persisted boot id and M3's `BOOT_COMPLETED` receiver. Every
|
||||
read-modify-write runs inside a DAO transaction, and the stopwatch run record
|
||||
is written whole in one DataStore edit, so concurrent writers cannot swallow
|
||||
each other's edits or persist a torn record. 151 JVM tests over fake DAOs and
|
||||
injected fake clocks; the migration test is the only thing needing a device.
|
||||
The app talks to four repository interfaces speaking domain types and Flows,
|
||||
and `ArchitectureRulesTest` fails the build if Room, an entity or a query
|
||||
string ever appears above `data/`. Time is a parameter rather than an ambient
|
||||
read: `WallClock` and `ElapsedRealtimeClock` are distinct types, so a running
|
||||
timer anchors on the monotonic clock and keeps a wall-clock value only as a
|
||||
post-reboot fallback, and the stopwatch keeps none at all. That leaves one
|
||||
gap: once a new boot's uptime climbs past a stored anchor the reboot goes
|
||||
unnoticed, so closing it needs a persisted boot id and M3's `BOOT_COMPLETED`
|
||||
receiver. Every read-modify-write runs inside a DAO transaction, and the
|
||||
stopwatch run record is written whole in one DataStore edit, so concurrent
|
||||
writers cannot swallow each other's edits or persist a torn record. 151 JVM
|
||||
tests over fake DAOs and injected fake clocks; the migration test is the only
|
||||
thing needing a device.
|
||||
|
||||
### ✅ M3 — Alarm engine
|
||||
The hard part (`PLAN.md` §4), still headless apart from the ring screen's
|
||||
@@ -140,13 +140,13 @@ plumbing.
|
||||
- Ringing foreground service: audio focus, `USAGE_ALARM`, volume ramp, vibration,
|
||||
ringtone URIs; survives process death and reboot mid-ring.
|
||||
- Full-screen-intent notification + `canUseFullScreenIntent()` handling, degrading
|
||||
to a high-priority heads-up notification that still rings — never to silence.
|
||||
to a high-priority heads-up notification that still rings, never to silence.
|
||||
- **Tests:** DST spring-forward and fall-back, every repeat configuration, skip,
|
||||
snooze-vs-next-occurrence. These are the tests the app lives or dies on.
|
||||
|
||||
✅ 326 JVM tests, 172 of them new. The engine is a plain Kotlin class that
|
||||
reaches Android through four interfaces it does not implement, so the
|
||||
behaviour the app lives or dies on is pinned without an emulator —
|
||||
behaviour the app lives or dies on is pinned without an emulator.
|
||||
`ArchitectureRulesTest` fails the build if `android.*` ever appears in
|
||||
`domain/`, `AlarmEngine.kt` or `system/`. Occurrences are generated by walking
|
||||
local dates through `ZonedDateTime.of` rather than by adding 24 hours to an
|
||||
@@ -155,43 +155,43 @@ plumbing.
|
||||
fall-back night. Asserted for Berlin and New York and across all 128 repeat
|
||||
masks. Two rules keep constant re-resolution honest: a two-minute grace window
|
||||
so a late fire still counts, and a `handled_occurrence` watermark that can
|
||||
suppress a re-fire but — because suppression also requires the candidate to be
|
||||
at-or-before now — can never silence the future. Skip needed a watermark of its
|
||||
suppress a re-fire but can never silence the future, because suppression also
|
||||
requires the candidate to be at-or-before now. Skip needed a watermark of its
|
||||
own, since a bare flag eats a second alarm once the skipped occurrence passes.
|
||||
At most one alarm rings and a newly-firing one takes over, so closing a cycle
|
||||
touches the shared ring and auto-silence slots only when that alarm is the one
|
||||
ringing; every write to `alarm_states` runs under one mutex, because a cold
|
||||
start triggered by the fire broadcast otherwise races its own reschedule pass.
|
||||
The "never to silence" chain is a testable invariant, not a hope: full-screen
|
||||
intent, else a heads-up notification that still rings, else the default
|
||||
ringtone, else forced vibration. Schema v2 adds `alarm_states`, and the boot id
|
||||
closes M2's blind spot for running timers **and** the stopwatch — a scope move
|
||||
from M7, recorded in `ARCHITECTURE.md` §5. Knowingly open: the migration and
|
||||
the other 12 instrumentation tests compile but have never run on hardware, as
|
||||
no device was attached; an auto-silenced alarm is silently missed, with no
|
||||
missed-alarm notification until someone asks for one.
|
||||
The "never to silence" chain is a testable invariant: full-screen intent, else
|
||||
a heads-up notification that still rings, else the default ringtone, else
|
||||
forced vibration. Schema v2 adds `alarm_states`, and the boot id closes M2's
|
||||
blind spot for running timers and the stopwatch, a scope move from M7
|
||||
recorded in `ARCHITECTURE.md` §5. Knowingly open: the migration and the other
|
||||
12 instrumentation tests compile but have never run on hardware, as no device
|
||||
was attached; an auto-silenced alarm is silently missed, with no missed-alarm
|
||||
notification until someone asks for one.
|
||||
|
||||
### ✅ M4 — App shell
|
||||
- Navigation host, four tabs, M3 navigation bar, adaptive rail on wide layouts.
|
||||
- The **live pill** (`PLAN.md` §9) — shared running-state surface, pause + stop
|
||||
- The **live pill** (`PLAN.md` §9): shared running-state surface, pause + stop
|
||||
from any tab.
|
||||
- The ring screen UI: over-lockscreen, turn-screen-on, snooze/dismiss, optional
|
||||
dismiss challenge that cannot strand the user.
|
||||
- Motion from the kit's `identity` module; predictive back.
|
||||
|
||||
✅ 480 JVM tests, 154 of them new, and still **no Robolectric** — which had to
|
||||
be earned rather than kept. Everything that decides something moved out of the
|
||||
composables: `ShellNavigation` (what a tab tap does to the back stack, and
|
||||
that back from a tab goes to Alarms while back from Alarms leaves the app),
|
||||
✅ 480 JVM tests, 154 of them new, and still no Robolectric. Keeping it that
|
||||
way meant moving everything that decides something out of the composables:
|
||||
`ShellNavigation` (what a tab tap does to the back stack, and that back from a
|
||||
tab goes to Alarms while back from Alarms leaves the app),
|
||||
`LivePillSelector` (which of several running things the pill is about),
|
||||
`ClockFormat`, and `ChallengeGate`. The adaptive part is
|
||||
`NavigationSuiteScaffold` taking its own default — the short bar on a phone,
|
||||
the wide rail on a tablet — rather than a hand-rolled width branch that would
|
||||
`NavigationSuiteScaffold` taking its own default (the short bar on a phone,
|
||||
the wide rail on a tablet) rather than a hand-rolled width branch that would
|
||||
miss the tabletop posture. The pill shows for paused and expired timers too,
|
||||
not only running ones: pausing from the pill must not make the pill disappear
|
||||
under the thumb that pressed it. Its value comes from the snapshot, so a timer
|
||||
that reaches zero flips to "finished" before M6's expiry write lands. The
|
||||
challenge's escape hatch opens on three wrong answers **or** sixty seconds of
|
||||
challenge's escape hatch opens on three wrong answers or sixty seconds of
|
||||
ringing, for every challenge kind, asserted as one parameterised invariant;
|
||||
snooze is never gated; a completed hold *is* the dismissal, and an
|
||||
accessibility click completes the hold, because a challenge TalkBack cannot
|
||||
@@ -200,7 +200,7 @@ plumbing.
|
||||
top-level tab draws no back arrow that would pop nothing.
|
||||
|
||||
Knowingly open: M4 ships no in-app way to *start* a timer or the stopwatch, so
|
||||
the live pill cannot be demonstrated by hand until M6/M7 — it is proven by its
|
||||
the live pill cannot be demonstrated by hand until M6/M7. It is proven by its
|
||||
unit tests and by an instrumentation test that seeds the repository directly,
|
||||
and no debug button was added to paper over it. The nine new instrumentation
|
||||
tests compile in the gate but have never run on hardware, as no device was
|
||||
@@ -213,18 +213,18 @@ List, create, edit, delete, enable/disable. Time picker, repeat-day selector,
|
||||
label, ringtone picker, vibrate, snooze settings, per-alarm overrides. Next-fire
|
||||
line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`.
|
||||
|
||||
✅ 624 JVM tests, 144 of them new, and **still no Robolectric** — kept for a
|
||||
✅ 624 JVM tests, 144 of them new, and still no Robolectric, kept for a
|
||||
whole *form*, which is where a screen usually reaches for one. Everything a
|
||||
tap decides is a pure object (`AlarmListRows`, `NextFireFormat`,
|
||||
`RepeatDaysSummary`, `AlarmDefaults`, `AlarmOverrides`, `RingtonePicker`,
|
||||
`AudioSourcePolicy`, `AlarmRoutes`) or a ViewModel method tested over the
|
||||
**real** `AlarmEngine`; three new `ArchitectureRulesTest` rules make that a
|
||||
real `AlarmEngine`; three new `ArchitectureRulesTest` rules make that a
|
||||
build fact rather than a habit.
|
||||
|
||||
The decisions worth knowing: the next-fire line comes from
|
||||
`AlarmEngine.upcoming`, which grew a `refresh: Flow<Unit>` cadence parameter
|
||||
so the countdown advances without a database write and the screen re-derives
|
||||
none of the engine's time arithmetic; the list is ordered by **time of day**,
|
||||
none of the engine's time arithmetic; the list is ordered by time of day,
|
||||
not by next fire, so rows never swap under a thumb at 07:00; every per-alarm
|
||||
override is an `OptionPicker` whose first row is "App default (On)", because
|
||||
the stored value is ternary and a `Switch` cannot say "I have not chosen";
|
||||
@@ -235,79 +235,79 @@ line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`.
|
||||
read-modify-write, so a one-shot alarm that disabled itself underneath an open
|
||||
editor is not re-enabled by the next unrelated tap.
|
||||
|
||||
The correctness find: disabling or deleting a **ringing** alarm used to clear
|
||||
`ringingSince` without stopping the ring service — whose auto-silence backstop
|
||||
then disarms itself on its own null guard — so the alarm would have sounded
|
||||
The correctness find: disabling or deleting a ringing alarm used to clear
|
||||
`ringingSince` without stopping the ring service (whose auto-silence backstop
|
||||
then disarms itself on its own null guard), so the alarm would have sounded
|
||||
until the 15-minute wake-lock timeout. Both new ViewModels now dismiss through
|
||||
the engine first, conditionally, so a merely snoozed alarm keeps its snooze.
|
||||
|
||||
floret-kit went to **0.6.0** for one change: `CollapsingScaffold` takes a
|
||||
floret-kit went to 0.6.0 for one change: `CollapsingScaffold` takes a
|
||||
`floatingActionButton` slot, forwarded to the underlying `Scaffold`, rather
|
||||
than each app re-deriving the inset maths in a `Box`.
|
||||
|
||||
Knowingly open: no hand reordering of alarms (the time order answers it, and
|
||||
`alarms` has no `sort_order` column), no multi-select or bulk delete, no undo
|
||||
chip after a delete (traded for a confirmation that names the alarm), no
|
||||
repeat presets, and no per-alarm auto-silence — still M10 at the earliest.
|
||||
repeat presets, and no per-alarm auto-silence (still M10 at the earliest).
|
||||
Editing the app-wide defaults every override row offers to follow is M10's
|
||||
settings screen. The seven new instrumentation tests compile in the gate but
|
||||
have **never run on hardware**, as no device was attached.
|
||||
have never run on hardware, as no device was attached.
|
||||
|
||||
### ✅ 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
|
||||
✅ 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
|
||||
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,
|
||||
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
|
||||
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
|
||||
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, 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
|
||||
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
|
||||
`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
|
||||
UI existed and pinned by no test; 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.
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -318,85 +318,85 @@ Elapsed-realtime anchored.
|
||||
"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`
|
||||
`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.
|
||||
have never run on hardware, as no device was attached; M5's seven carry the
|
||||
same caveat.
|
||||
|
||||
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:
|
||||
Two cases in the new suite are left red. Both are defects in the test, not in
|
||||
the code, so they are a design conversation rather than a behaviour gap:
|
||||
`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.
|
||||
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
|
||||
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);
|
||||
The post-reboot repair is not here: M3 took it (`ARCHITECTURE.md` §5), and
|
||||
M7 owns only how the paused result is presented.
|
||||
|
||||
✅ 968 JVM tests, 125 of them new, and **still no Robolectric** — kept now for
|
||||
✅ 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
|
||||
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
|
||||
`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
|
||||
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**,
|
||||
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**
|
||||
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
|
||||
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`**,
|
||||
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
|
||||
`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
|
||||
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
|
||||
to be banked as well as 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.
|
||||
bank the same floored reading; otherwise 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**:
|
||||
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
|
||||
@@ -409,86 +409,86 @@ M7 owns only how the paused result is presented.
|
||||
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.
|
||||
the gate but have never run on hardware, as no device was attached; M5's
|
||||
seven and M6's four carry the same caveat.
|
||||
|
||||
### ✅ 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
|
||||
built from `MaterialShapes` / `androidx.graphics.shapes` is 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,
|
||||
✅ 1 139 JVM tests, 171 of them new, and still no Robolectric, kept now
|
||||
for a fourth screen. 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
|
||||
`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**
|
||||
`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
|
||||
The decisions worth knowing: there is no engine and must not be one, because 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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**,
|
||||
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
|
||||
`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
|
||||
`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 —
|
||||
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
|
||||
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
|
||||
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;
|
||||
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
|
||||
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.
|
||||
in the gate but have never run on hardware, as no device was attached;
|
||||
M5's seven, M6's four and M7's four carry the same caveat.
|
||||
|
||||
### ✅ M9 — System interop
|
||||
The full `android.provider.AlarmClock` contract (`PLAN.md` §6): `SET_ALARM`,
|
||||
@@ -496,103 +496,103 @@ The full `android.provider.AlarmClock` contract (`PLAN.md` §6): `SET_ALARM`,
|
||||
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
|
||||
✅ 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.
|
||||
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`. It is 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
|
||||
`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
|
||||
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, and 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,
|
||||
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
|
||||
cannot tell "absent" from "wrong type"; `SKIP_UI` never means skip
|
||||
validation, so 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
|
||||
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
|
||||
`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
|
||||
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
|
||||
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.
|
||||
what "next-alarm publishing" means to a user.
|
||||
|
||||
**The schema went to v3**: `delete_after_use` on `alarms` and on `timers`, two
|
||||
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.
|
||||
and `2.json` untouched. The contract says in two places that a `SKIP_UI`
|
||||
alarm or timer should be removed once dismissed, and without the column 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
|
||||
`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"
|
||||
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
|
||||
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,
|
||||
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
|
||||
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
|
||||
was given; and `AlarmScheduler.systemNextAlarm()` is still caller-less, since 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.
|
||||
two new migration cases compile in the gate but have never run on
|
||||
hardware, as no device was attached; M5's seven, M6's four, M7's four and
|
||||
M8's four carry the same caveat.
|
||||
|
||||
### ✅ M10 — Settings, backup, the self-check, and widgets
|
||||
- Settings composed from kit components: theme, dynamic colour, language,
|
||||
alarm/timer/clock defaults, about + crash reporting.
|
||||
- JSON backup export/import via SAF (`PLAN.md` §7), schema documented in
|
||||
`docs/BACKUP.md`.
|
||||
- The **"why might my alarm not ring?"** self-check screen (`PLAN.md` §4) —
|
||||
- The "why might my alarm not ring?" self-check screen (`PLAN.md` §4):
|
||||
real device state, deep links to the exact settings pages.
|
||||
- Three Glance home-screen widgets — next alarm, timer, clock (`PLAN.md` §9).
|
||||
- Three Glance home-screen widgets: next alarm, timer, clock (`PLAN.md` §9).
|
||||
- **Tests:** backup round-trip and unknown-field tolerance; self-check rule
|
||||
logic per permission/state combination; widget model selection/formatting.
|
||||
|
||||
@@ -608,13 +608,13 @@ reliability soak across a few real nights. Then 1.0.0.
|
||||
|
||||
Deferred deliberately, in rough priority order:
|
||||
|
||||
- **Dock / screensaver mode** — a `DreamService` full-screen clock. Cheapest big
|
||||
- **Dock / screensaver mode**: a `DreamService` full-screen clock. Cheapest big
|
||||
win, and the surface people look at most.
|
||||
- **Quick Settings tile** — timer control and next alarm from the shade.
|
||||
- **Bedtime / sleep schedule** — wind-down and wake, with DND handoff. A feature
|
||||
- **Quick Settings tile**: timer control and next alarm from the shade.
|
||||
- **Bedtime / sleep schedule**: wind-down and wake, with DND handoff. A feature
|
||||
in its own right, realistically v2.
|
||||
- **`core-notification` in floret-kit** — revisit once Clockula's real
|
||||
- **`core-notification` in floret-kit**: revisit once Clockula's real
|
||||
notification surface is known (`PLAN.md` §3).
|
||||
- **Migrate Calendula and Agendula onto `core-prefs` + `core-di`** — tracked in
|
||||
- **Migrate Calendula and Agendula onto `core-prefs` + `core-di`**: tracked in
|
||||
the kit's roadmap, not here.
|
||||
- **Wear OS companion** — unscoped.
|
||||
- **Wear OS companion**: unscoped.
|
||||
Reference in new issue
Block a user