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:
+354
@@ -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.
|
||||
@@ -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.
|
||||
+164
@@ -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.
|
||||
Reference in New Issue
Block a user