10 KiB
Clockula — roadmap
The status view, and the work queue. Design rationale for every decision here
lives in PLAN.md; the shape of the code as it stands will live in
ARCHITECTURE.md from M2 onward.
Status legend: ✅ done · 🚧 in progress · ⬜ not started
Current state (one line)
✅ M2 done. Clockula owns its storage: a Room database of four tables with
the schema exported and committed, plain-Kotlin domain models behind four
Flow-based repository interfaces, DataStore preferences, Hilt wiring, and the
first ARCHITECTURE.md. M3 (the alarm engine — next-fire resolution over
repeat masks and DST, AlarmScheduler, the receivers, and the ringing service)
is next, and is the hard one.
How the loop works through this
One milestone per iteration, auto-continuing. Each iteration:
- Pick the first milestone not marked ✅.
- Implement it completely — every checklist item under it.
- Build, and run the tests the milestone names. A milestone is not done until they pass.
- Commit (floret-kit submodule first where it changed, then Clockula's pointer bump — separate commits, granular, lowercase area prefix).
- Mark the milestone ✅ here, update Current state, and commit that too.
- Continue to the next milestone.
Rules that hold across every iteration:
- Never let a Room entity or
@Querystring 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.mdandCHANGELOG.mdare 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,vcsInfooff on release, no foojay resolver. -
floret-kit as a git submodule +
includeBuild, with the gitignoredfloret-kit/local.propertiesarrangement documented. -
Hilt, Compose,
MainActivity,FloretExpressiveThemereseeded to#6B7A5C(ui/theme/Color.kt+Theme.kt), hand-tuned light/dark schemes. -
core-crashwired;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 + Calendulaplayjob),renovate.yml, issue/PR templates, F-Droid metadata, fastlane tree. -
README.md,LICENSE(MIT),CHANGELOG.md,.editorconfig,.gitattributes,.gitignore,docs/README.mdindex. -
Done when: it builds, installs, shows a themed placeholder, and CI is green.
✅
assembleDebug,lintDebugandtestDebugUnitTestall pass;scripts/check_reproducible_release.shreports every invariant holding. Two notes for later: the store listing andfastlanechangelogs are written but no screenshots exist yet (M11), anddocs/ARCHITECTURE.mdis still unwritten by design — M2 writes it, once there is an architecture to describe.
✅ 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()) andcore-di(@IoDispatcher+ provider), each with tests, module docs,CHANGELOGandROADMAP/ARCHITECTUREupdates. -
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 typedPref<T>/PrefStoreover DataStore; reads degrade to the default instead of throwing, andflow()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.core-dicarrieshilt-compileritself, since an@InstallInmodule only reaches a consumer's component if its own library was processed; Clockula'sassembleDebugis the integration test for that binding. Clockula's theme now reads the stored preference, with the DataStore built on a corruption handler so an unreadable prefs file cannot brick startup.
✅ 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. -
Done when: the schema is exported and committed, no Room type or query string appears above
data/, and the build, lint and unit tests pass.✅ Four tables (
alarms,timers,world_clocks,stopwatch_laps), schema v1 exported toapp/schemasso migrations can be tested from the beginning. The seam that matters: the app talks to four repository interfaces speaking domain types and Flows, andArchitectureRulesTestfails the build if Room, an entity or a query string ever appears abovedata/. Time is a parameter rather than an ambient read —WallClockandElapsedRealtimeClockare 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'sBOOT_COMPLETEDreceiver. 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
plumbing.
- Next-fire resolution: local time-of-day + repeat mask → next instant in the
device zone;
skipNextOccurrence; snooze. AlarmScheduleroversetAlarmClock, single-next-alarm registration,USE_EXACT_ALARMwith aSCHEDULE_EXACT_ALARMfallback 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
identitymodule; 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
DreamServicefull-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-notificationin 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.