From 052c7b177d64d28371f2aa986dbcdb5008a2ff28 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 11 Sep 2026 11:27:57 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20lock=20the=20Clockula=20spec=20?= =?UTF-8?q?=E2=80=94=20PLAN,=20ROADMAP,=20docs=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01L94fydiJC37LtxVusNQBDy --- docs/PLAN.md | 354 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/README.md | 31 +++++ docs/ROADMAP.md | 164 ++++++++++++++++++++++ 3 files changed, 549 insertions(+) create mode 100644 docs/PLAN.md create mode 100644 docs/README.md create mode 100644 docs/ROADMAP.md diff --git a/docs/PLAN.md b/docs/PLAN.md new file mode 100644 index 0000000..ba0bd9f --- /dev/null +++ b/docs/PLAN.md @@ -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:")`, +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. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..06fada2 --- /dev/null +++ b/docs/README.md @@ -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 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). | + +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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..7b9455b --- /dev/null +++ b/docs/ROADMAP.md @@ -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.