docs: lock the Clockula spec — PLAN, ROADMAP, docs index

Third floret after Calendula and Agendula: a Material 3 Expressive clock app
(alarms, timers, stopwatch, world clock).

The family thesis doesn't survive contact with a clock — there is no open
provider behind one — so PLAN opens by saying so plainly and relocates the
open-standards commitment to the three places it can actually live: the full
android.provider.AlarmClock intent contract, IANA tzdata, and a documented
JSON backup format.

Locked: four surfaces and no extras in v1, Room + DataStore + JSON export,
maximum alarm reliability including a self-check diagnostics screen, seed
#6B7A5C (completing the family's Calendula→Agendula colour rotation), four
tabs plus a live running-state pill, Codeberg-canonical from commit one, and
floret-kit from day one with core-prefs + core-di extracted up front.

ROADMAP carries M0–M11 as an ordered work queue with per-milestone done
criteria, written to be executed one milestone per loop iteration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L94fydiJC37LtxVusNQBDy
This commit is contained in:
Jean-Luc Makiola
2026-09-11 11:27:57 +02:00
co-authored by Claude Opus 5
commit 052c7b177d
3 changed files with 549 additions and 0 deletions
+354
View File
@@ -0,0 +1,354 @@
# Clockula — implementation plan
> 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).
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).
Identifiers: `applicationId = de.jeanlucmakiola.clockula`, app name **Clockula**,
licence MIT, `minSdk 29` / `targetSdk 36` / `compileSdk 37`.
---
## 0. The thesis — and the honest asterisk
The family thesis is *a nice M3-Expressive front end over open backends, no
reinvented storage or sync*. Calendula fronts `CalendarContract`; Agendula speaks
the dmfs `TaskContract`.
**Clockula cannot do that, and pretending otherwise would be a lie.** There is no
open provider behind a clock. Alarms, timers and laps are the app's own data, and
Clockula is therefore the family's **first app with its own storage by design**,
not as a fallback posture.
So the open-standards commitment is *relocated*, not dropped. Clockula honours it
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.
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
format via SAF (§7). Data you can't take with you isn't really yours.
### The one rule that keeps this honest
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
Agendula swap its whole storage posture without touching the UI.
---
## 1. Locked decisions
| # | Decision | Why |
|---|---|---|
| 1 | **Four surfaces in v1**: alarms, timers, stopwatch, world clock. | The complete stock-clock feature set. Anything less reads as a toy next to the app it wants to replace. |
| 2 | **No extras in v1.** No widget, no QS tile, no screensaver, no bedtime. | v1 is already large. All four land in the roadmap as post-v1 milestones. Better a tight, finished v1 than five half-surfaces. |
| 3 | **Full `AlarmClock` intent contract** + next-alarm publishing via `setAlarmClock`. | §6. This is the thesis. |
| 4 | **Room + DataStore + JSON export.** | Repeat rules, per-alarm overrides and lap history want a real schema. Prefs stay DataStore. Backup is an open documented format. |
| 5 | **Maximum alarm reliability**, including a self-check diagnostics screen. | §4. An alarm that doesn't ring is a dead app. This is the whole value proposition, not a nice-to-have. |
| 6 | **Seed `#6B7A5C`** (muted sage). | §8. Completes the family rotation. |
| 7 | **Four tabs + a live pill.** | §9. Stock-familiar navigation; the pill fixes the stock clock's worst flaw. |
| 8 | **Codeberg-canonical from the first commit.** | §10. No Gitea-canonical phase to migrate out of later. |
| 9 | **floret-kit consumed from day one**, and Clockula pays the `core-prefs` + `core-di` extraction up front. | §3. The kit's own roadmap names a third app as the trigger. We are it. |
---
## 2. What transfers from the siblings
Agendula's layering is the template. Lift verbatim or near-verbatim:
| Area | From | Change for Clockula |
|---|---|---|
| Gradle setup, version catalog, `settings.gradle.kts`, repro rules | Agendula | Drop `:provider`; add Room + KSP |
| CI / release / translations / renovate pipeline | Agendula spine + Calendula's `play` job | §10 |
| F-Droid metadata, fastlane tree, issue/PR templates | Agendula | Reword |
| `core-crash` wiring, `core-locale` + `locales_config.xml` | Agendula | Reword |
| Theme structure (`ui/theme/Color.kt` + `Theme.kt` over `FloretExpressiveTheme`) | Agendula | Reseed to `#6B7A5C` |
| Settings composition from `GroupedSurface` / `GroupedRow` / `OptionPicker` | Agendula | Clock-specific domain |
| Export-via-SAF module shape (`data/export`, `DocumentFile`) | Agendula | JSON instead of iCalendar |
| Test stack: JUnit 5 + Truth + Turbine | Agendula | — |
**Deliberately not transferred:** Agendula's reminder *scheduler*. The kit
roadmap is explicit that schedulers stay app-local, and Clockula's needs
(exact alarms, full-screen intents, a ringing foreground service, DST-correct
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 —
`includeBuild("floret-kit")`, `implementation("de.jeanlucmakiola.floret:<module>")`,
with a gitignored `clockula/floret-kit/local.properties` locally and
`ANDROID_HOME` in CI.
**Used from day one:** `identity` (theme factory + motion + predictive back),
`components` (`GroupedSurface`/`GroupedRow`, `CollapsingScaffold`, `OptionPicker`,
`OptionCard`, `DialogControls`, `AboutCard`, `LanguagePickerRow`, `pastelize`),
`core-locale`, `core-crash`, `core-time`.
**Extracted by us (M1):** `core-prefs` (`ThemeMode`, `dynamicColor`, the typed
DataStore wrapper, `toEnum()`) and `core-di` (`@IoDispatcher` + provider). These
are pure relocation with a known shape, and the kit roadmap defers them only
"until a third app makes the duplication hurt". Writing them a third time is that
moment. Clockula consumes the extracted modules; **migrating Calendula and
Agendula onto them is a separate follow-up**, tracked in the kit's roadmap, not
in this loop.
**Deliberately deferred:** `core-notification`. Clockula's notification needs
(full-screen intents, ringing services, media-session-grade audio focus) are far
heavier than either sibling's. Designing a shared API from one speculative
consumer would produce a leaky abstraction. Revisit after v1, when the real
surface is known.
---
## 4. The alarm engine — the hard part
Everything else in this app is a readout. This is the engineering.
### Scheduling
- An alarm stores a **local time-of-day + a repeat-day set**, never an absolute
instant. The next fire time is *resolved* against the device's current zone
every time it is scheduled.
- Re-resolve on: fire, snooze, edit, enable/disable, `BOOT_COMPLETED`,
`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`.
`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
`USE_EXACT_ALARM` (install-granted for apps whose core function is an alarm
clock) with a `SCHEDULE_EXACT_ALARM` fallback path.
- Only the **single next** alarm is registered with `AlarmManager` at a time;
firing re-resolves and registers the following one. Registering all of them
invites drift and quota trouble.
### Ringing
`AlarmManager``BroadcastReceiver` → foreground service (which owns audio +
vibration and survives the receiver's 10-second window) → full-screen-intent
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.
- 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.
- Snooze with a configurable interval and a snooze limit; dismiss; an optional
dismiss challenge (maths / hold) that must never be able to strand the user.
- The ring activity shows over the lockscreen (`setShowWhenLocked`,
`setTurnScreenOn`) and holds a wake lock for the ring duration only.
- A ringing alarm survives process death and reboot mid-ring.
### Diagnosability (the self-check screen)
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, DND policy access, whether the OEM has
Clockula under an aggressive app-killer, and the next scheduled fire time as the
system sees it (`getNextAlarmClock()`). Where possible each row deep-links to the
exact settings page. This screen is a v1 feature, not a support afterthought.
---
## 5. Data model
Room, schema exported and version-controlled, migrations tested from v1.
- **`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
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
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
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.
World clocks are IANA zone IDs, with display names and city labels resolved
through ICU (`android.icu`) so they localise with the app language. No bundled
city database to rot.
---
## 6. System interop — the `AlarmClock` contract
Clockula handles, in full:
| Intent | Behaviour |
|---|---|
| `ACTION_SET_ALARM` | `EXTRA_HOUR`, `EXTRA_MINUTES`, `EXTRA_MESSAGE`, `EXTRA_DAYS`, `EXTRA_RINGTONE`, `EXTRA_VIBRATE`, `EXTRA_SKIP_UI`. Honours `SKIP_UI` only when the extras fully specify the alarm. |
| `ACTION_SET_TIMER` | `EXTRA_LENGTH`, `EXTRA_MESSAGE`, `EXTRA_SKIP_UI`. |
| `ACTION_SHOW_ALARMS` / `ACTION_SHOW_TIMERS` | Deep-link to the tab. |
| `ACTION_DISMISS_ALARM` / `ACTION_SNOOZE_ALARM` | Act on the firing or next alarm, with the documented search-mode extras. |
Plus `getNextAlarmClock()` participation via `setAlarmClock`, so the system
status bar and lockscreen show Clockula's next alarm like any stock clock.
**Every extra is untrusted input from another app.** Hours, minutes, day sets,
lengths and URIs are validated and clamped at the intent boundary before they
reach a repository, and a malformed intent opens the editor pre-filled rather
than writing anything. `SKIP_UI` never means "skip validation".
---
## 7. Backup format
`clockula-backup-v1.json`, written to a user-chosen folder via SAF
(`DocumentFile`), mirroring Agendula's export module shape. Top-level
`{ "format": "clockula-backup", "version": 1, "exportedAt": …, "alarms": […],
"timers": […], "worldClocks": […], "settings": {…} }`. The schema is **documented
in the repo**, versioned, and import is tolerant of unknown fields and strict
about known ones. Ringtone URIs are exported as-is with a documented fallback
when the target device can't resolve them.
---
## 8. Identity
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,
third position. The cycle closes at three, which is a constraint worth keeping
rather than a limit to work around.
Structure follows the siblings: `FloretExpressiveTheme` from the kit's `identity`
module, dynamic colour on by default with the seed-derived scheme as the
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.
- 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 —
not sprinkled everywhere.
A clock app is mostly one enormous number. That readout is where the design
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
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.
Adaptive: navigation rail on wide layouts, per M3 adaptive guidance.
---
## 10. Repository & pipeline
**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,
adjusting `SKIP_RE`.
- `.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
`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
`github.repository_owner == 'makiolaj'` guard.
- Issue templates, PR template, F-Droid metadata, fastlane tree.
Reproducibility rules carry over verbatim and are load-bearing: **no foojay
toolchain resolver anywhere** (not even in a comment), `vcsInfo { include = false }`
on release, `submodules: recursive` in checkout, and the repro guard scanning the
floret-kit submodule's Gradle scripts.
---
## 11. ClockMaster — how we use it, and the licence line
[PranshulGG/ClockMaster](https://github.com/PranshulGG/ClockMaster) is an
archived, unmaintained **Apache-2.0** Material Expressive clock app. It is a
useful reference for the parts of an alarm app that are tedious to rediscover.
**Apache-2.0 code cannot be relicensed to MIT.** So:
- **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
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.
- Every such decision is made **consciously and recorded**, never by drifting
into a copy-paste.
**Worth studying:** `helpers/alarmHelper.kt` (scheduling), `receiver/alarmReceiver.kt`,
`services/alarmService.kt` + `TimerAlarmService.kt` (ring/foreground lifecycle),
`helpers/alarmSoundPicker.kt` (`RingtoneManager` plumbing), the timezone
DAO/repository, and the Room entity shapes as a sanity check on §5.
**Deliberately not taken:**
- 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 **settings/tile component vocabulary** (`ui/components/tiles/*`,
`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
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
ClockMaster.
---
## 12. Testing posture
JUnit 5 + Truth + Turbine, matching Agendula. Non-negotiable unit coverage:
- Next-fire resolution across DST spring-forward and fall-back, every repeat-day
configuration, `skipNextOccurrence`, and snooze interaction with the next
occurrence.
- Elapsed-realtime vs wall-clock behaviour for timers and the stopwatch under a
simulated user clock change.
- Intent-extra validation at the `AlarmClock` boundary, including hostile input.
- Backup round-trip: export → import → identical state, plus forward-compatible
handling of unknown fields.
- Room migrations, from v1 onward.
+31
View File
@@ -0,0 +1,31 @@
# Clockula — documentation
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
a clock. What that costs and how the open-standards commitment is honoured anyway
is the first section of [`PLAN.md`](PLAN.md).
## Index
| 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 M0M11 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). |
Also: [`../CHANGELOG.md`](../CHANGELOG.md) (Keep a Changelog format; tag sections
feed the release notes).
## How the docs relate
- **PLAN** is the design decisions (stable; the "why").
- **ARCHITECTURE** is the current shape of the code (kept in sync as it grows).
- **ROADMAP** is the moving status layer (updated as milestones land).
- **RELEASING** is the operational runbook.
+164
View File
@@ -0,0 +1,164 @@
# Clockula — roadmap
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.
Status legend: ✅ done · 🚧 in progress · ⬜ not started
---
## Current state (one line)
**Nothing built yet.** The spec is locked (`PLAN.md`); M0 is next.
---
## How the loop works through this
One milestone per iteration, auto-continuing. Each iteration:
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).
5. Mark the milestone ✅ here, update **Current state**, and commit that too.
6. Continue to the next milestone.
Rules that hold across every iteration:
- **Never let a Room entity or `@Query` string leak above the data layer**
(`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
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.
---
## v1 milestones
### ⬜ M0 — Skeleton & pipeline
The project exists, builds, is themed, and ships itself.
- Gradle project: `settings.gradle.kts`, version catalog copied from Agendula
(drop `:provider`, add Room + KSP), AGP 9.x conventions, `vcsInfo` off on
release, no foojay resolver.
- floret-kit as a git submodule + `includeBuild`, with the gitignored
`floret-kit/local.properties` arrangement documented.
- Hilt, Compose, `MainActivity`, `FloretExpressiveTheme` reseeded to `#6B7A5C`
(`ui/theme/Color.kt` + `Theme.kt`), hand-tuned light/dark schemes.
- `core-crash` wired; `core-locale` + `res/xml/locales_config.xml`.
- Codeberg repo, canonical from the first commit. Full pipeline per `PLAN.md` §10:
`ci.yaml`, `translations.yaml`, `release.yaml` (Agendula spine + Calendula `play`
job), `renovate.yml`, issue/PR templates, F-Droid metadata, fastlane tree.
- `README.md`, `LICENSE` (MIT), `CHANGELOG.md`, `.editorconfig`, `.gitattributes`,
`.gitignore`, `docs/README.md` index.
- **Done when:** it builds, installs, shows a themed placeholder, and CI is green.
### ⬜ M1 — floret-kit: `core-prefs` + `core-di`
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.
- **Done when:** the kit's tests pass, Clockula builds against the new modules,
and both repos are committed (submodule first, then the pointer bump).
### ⬜ M2 — Data layer
The whole storage stack, headless. No UI.
- Room: `alarms`, `timers`, `world_clocks`, `stopwatch_laps` (`PLAN.md` §5),
schema exported and committed, DAOs.
- Domain models (plain Kotlin) + mappers. Repositories exposing Flows.
- DataStore prefs (theme, dynamic colour, defaults, stopwatch running state).
- DI modules.
- **Tests:** mappers, repository behaviour, the elapsed-realtime vs wall-clock
distinction under a simulated clock change.
- Write the first `ARCHITECTURE.md`.
### ⬜ M3 — Alarm engine
The hard part (`PLAN.md` §4), still headless apart from the ring screen's
plumbing.
- Next-fire resolution: local time-of-day + repeat mask → next instant in the
device zone; `skipNextOccurrence`; snooze.
- `AlarmScheduler` over `setAlarmClock`, single-next-alarm registration,
`USE_EXACT_ALARM` with a `SCHEDULE_EXACT_ALARM` fallback path.
- Receivers: fire, `BOOT_COMPLETED`, `TIME_SET`, `TIMEZONE_CHANGED`,
`MY_PACKAGE_REPLACED`.
- 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.
- **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.
### ⬜ 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
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.
### ⬜ M5 — Alarms screen
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`.
### ⬜ 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.
### ⬜ 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.
### ⬜ M8 — World clock
IANA zone list with search, ICU-localised city and zone display names, offset and
day-difference relative to home, reorder, home-zone handling. The analog face
built from `MaterialShapes` / `androidx.graphics.shapes` — the app's one
deliberate showpiece.
### ⬜ 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.
### ⬜ M10 — Settings, backup, and the self-check
- 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 the repo.
- The **"why might my alarm not ring?"** self-check screen (`PLAN.md` §4) —
real device state, deep links to the exact settings pages.
- **Tests:** backup round-trip and unknown-field tolerance.
### ⬜ M11 — Release readiness
Translations wired to Weblate, fastlane metadata and screenshots, F-Droid
reproducibility guard verified against the submodule, instrumentation smoke
tests, accessibility pass (TalkBack on the ring screen especially), on-device
reliability soak across a few real nights. Then 1.0.0.
---
## Post-v1
Deferred deliberately, in rough priority order:
- **Dock / screensaver mode** — a `DreamService` full-screen clock. Cheapest big
win, and the surface people look at most.
- **Glance home-screen widget** — next alarm and running timer.
- **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
notification surface is known (`PLAN.md` §3).
- **Migrate Calendula and Agendula onto `core-prefs` + `core-di`** — tracked in
the kit's roadmap, not here.
- **Wear OS companion** — unscoped.