docs: plain-language pass over every doc
Removes AI-writing patterns across the docs, store listing and templates: dash-heavy rhythm, not-X-but-Y contrasts, one-line kickers and decorative bold. Facts, code, links and headings are unchanged. Also fixes leftovers copied from the sibling apps: the crash report template promised "no calendar content", and CONTRIBUTING described Agendula's task-provider layout, test seams and commit style.
This commit is contained in:
1 parent
881f6b1286
commit
e870918a03
11 files changed
+697
-697
No files matched your search
@@ -6,8 +6,8 @@ contact_links:
|
||||
- name: Translate Clockula
|
||||
url: https://weblate.dev.jeanlucmakiola.de/engage/clockula/
|
||||
about: >-
|
||||
Translations are managed on Weblate, not here — it owns every values-*
|
||||
file, so a hand-edited translation gets overwritten on the next sync.
|
||||
Translations are managed on Weblate, which owns every values-* file, so
|
||||
a translation edited by hand here gets overwritten on the next sync.
|
||||
No coding needed: pick or request a language and translate in the browser.
|
||||
|
||||
- name: Contributing guide
|
||||
@@ -20,13 +20,12 @@ contact_links:
|
||||
url: https://codeberg.org/jlmakiola/clockula/src/branch/main/README.md
|
||||
about: >-
|
||||
Open the app's own self-check first (Settings → Why might my alarm not
|
||||
ring?). It reports what your device is actually doing — battery
|
||||
optimisation, notification and full-screen-intent permissions, the next
|
||||
alarm as the system sees it — and that report is what makes such an issue
|
||||
actionable.
|
||||
ring?). It reports what your device is doing (battery optimisation,
|
||||
notification and full-screen-intent permissions, the next alarm as the
|
||||
system sees it). Include that report so the issue can be acted on.
|
||||
|
||||
- name: Scope and roadmap
|
||||
url: https://codeberg.org/jlmakiola/clockula/src/branch/main/docs/ROADMAP.md
|
||||
about: >-
|
||||
What's built, what's planned, and what's deliberately out of scope —
|
||||
check here before requesting a feature.
|
||||
What's built, what's planned, and what's deliberately out of scope.
|
||||
Check here before requesting a feature.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Crash report
|
||||
about: Report a crash. Clockula can capture this for you (Settings → Report a problem, or the prompt after a crash) — it copies the report to your clipboard and prefills this form.
|
||||
about: Report a crash. Clockula can capture this for you (Settings → Report a problem, or the prompt after a crash). It copies the report to your clipboard and prefills this form.
|
||||
title: "Crash: "
|
||||
labels:
|
||||
- bug
|
||||
@@ -11,10 +11,10 @@ labels:
|
||||
<!--
|
||||
Thanks for reporting a crash in Clockula!
|
||||
|
||||
If the app prefilled this for you, the crash report is already below — just add
|
||||
what you were doing and submit. Otherwise, paste the report from your clipboard
|
||||
If the app prefilled this for you, the crash report is already below. Add what
|
||||
you were doing and submit. Otherwise, paste the report from your clipboard
|
||||
into the code block. The report contains only app/Android/device versions and the
|
||||
stack trace — no personal data or calendar content.
|
||||
stack trace, with no personal data and none of your alarms, timers or clocks.
|
||||
-->
|
||||
|
||||
### What happened
|
||||
|
||||
@@ -9,7 +9,7 @@ labels:
|
||||
### What would you like Clockula to do?
|
||||
|
||||
|
||||
### Why — what problem does it solve?
|
||||
### What problem would it solve?
|
||||
|
||||
|
||||
### Anything else
|
||||
|
||||
@@ -5,7 +5,7 @@ Please skim CONTRIBUTING.md if you haven't:
|
||||
https://codeberg.org/jlmakiola/clockula/src/branch/main/CONTRIBUTING.md
|
||||
|
||||
Two things it's easy to get wrong:
|
||||
• The one architectural rule — Room entities, DAOs and `@Query` strings never
|
||||
• The one architectural rule: Room entities, DAOs and `@Query` strings never
|
||||
leak above the data layer. Everything else talks to a repository.
|
||||
• Don't bump `versionName` / `versionCode`. That bump reaching `main` is what
|
||||
cuts a release, so it belongs only in a release PR.
|
||||
@@ -16,7 +16,7 @@ Two things it's easy to get wrong:
|
||||
|
||||
### Why
|
||||
|
||||
<!-- Closes #123 — link the issue this implements or fixes. -->
|
||||
<!-- Link the issue this implements or fixes, e.g. Closes #123. -->
|
||||
|
||||
|
||||
### How it was tested
|
||||
@@ -24,7 +24,7 @@ Two things it's easy to get wrong:
|
||||
<!--
|
||||
Which of these ran green, and anything you exercised by hand. On-device notes
|
||||
are especially useful for UI changes, and essential for anything touching the
|
||||
alarm engine — an alarm that doesn't ring is the one bug that matters most, and
|
||||
alarm engine. An alarm that doesn't ring is the one bug that matters most, and
|
||||
it only ever reproduces on a real device overnight.
|
||||
|
||||
./gradlew lintDebug :app:testDebugUnitTest :app:assembleDebug
|
||||
|
||||
+27
-24
@@ -1,8 +1,8 @@
|
||||
# Contributing to Clockula
|
||||
|
||||
Thanks for your interest in Clockula — a Material 3 Expressive clock app:
|
||||
Thanks for your interest in Clockula, a Material 3 Expressive clock app for
|
||||
alarms, timers, stopwatch and world clock. Unlike its siblings it owns its
|
||||
storage, because there is no open provider behind a clock. Before diving in, skim [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
||||
storage, because there is no open provider behind a clock. Before you start, skim [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
||||
(how it's built), [`docs/ROADMAP.md`](docs/ROADMAP.md) (what's next), and
|
||||
[`docs/PLAN.md`](docs/PLAN.md) (the design rationale). This file covers the
|
||||
practical how.
|
||||
@@ -15,14 +15,14 @@ layer.** Clockula owns its storage, which makes this rule more important here
|
||||
than in the siblings, not less: it is the only thing keeping the schema a
|
||||
private implementation detail rather than the app's de-facto domain model. If a
|
||||
change would expose a Room type to a ViewModel or the UI, it's in the wrong
|
||||
layer — see [`docs/PLAN.md`](docs/PLAN.md) §0.
|
||||
layer; see [`docs/PLAN.md`](docs/PLAN.md) §0.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- JDK 17
|
||||
- Android SDK: compileSdk 37, build-tools 36.0.0 (the Gradle wrapper handles AGP/Kotlin)
|
||||
- A real device for anything touching the alarm engine. An emulator will not
|
||||
tell you whether an alarm survives doze, a reboot, or an OEM battery-saver —
|
||||
tell you whether an alarm survives doze, a reboot, or an OEM battery-saver,
|
||||
and that is the only question that matters about an alarm.
|
||||
|
||||
## Build, test, lint
|
||||
@@ -43,11 +43,11 @@ before opening a PR. Keep CI green.
|
||||
owned by a self-hosted Weblate that writes to this repository directly, and a
|
||||
hand-edit is overwritten on the next sync.
|
||||
|
||||
→ **[Translate Clockula on Weblate](https://weblate.dev.jeanlucmakiola.de/engage/clockula/)**
|
||||
**[Translate Clockula on Weblate](https://weblate.dev.jeanlucmakiola.de/engage/clockula/)**
|
||||
|
||||
Adding a *new* English string to `values/strings.xml` is normal PR work; Weblate
|
||||
picks it up and offers it to translators. Partial translations are expected and
|
||||
fine — missing keys are informational. Stale and orphaned keys are not, so run
|
||||
fine; missing keys are informational. Stale and orphaned keys are not, so run
|
||||
|
||||
```sh
|
||||
python3 scripts/check_translations.py
|
||||
@@ -57,64 +57,67 @@ before pushing. It reports those more clearly than lint's `MissingTranslation`
|
||||
does, and it's what the `Translations` check runs on every PR.
|
||||
|
||||
A new language also needs one `<locale>` line in
|
||||
`app/src/main/res/xml/locales_config.xml` — that file is the single source of
|
||||
`app/src/main/res/xml/locales_config.xml`. That file is the single source of
|
||||
truth for both the in-app picker and the Android 13+ per-app language setting.
|
||||
|
||||
## Where to put code
|
||||
|
||||
| Layer | Lives in | Rule of thumb |
|
||||
|---|---|---|
|
||||
| Pure logic (models, filtering, sorting, form validation, date maths) | `domain/` | No Android imports — must be JVM-unit-testable. |
|
||||
| Provider access | `data/tasks/` | The only place that knows about the provider. New provider work goes through `TasksDataSource`. |
|
||||
| Reminders, prefs, DI, demo data | `data/reminders/`, `data/prefs/`, `data/di/`, `data/demo/` | |
|
||||
| Pure logic (models, scheduling and ring policies, formatting, the self-check rules) | `domain/` | No Android imports; must be JVM-unit-testable. |
|
||||
| Storage | `data/` (`db/`, `alarms/`, `timers/`, `stopwatch/`, `worldclocks/`, `prefs/`, `backup/`) | The only place that knows Room or DataStore exists. Everything above it goes through a repository interface. |
|
||||
| Engines and their Android seams | `alarm/`, `timer/`, `stopwatch/`, `ring/`, `interop/`, `widget/` | The engine is plain Kotlin; AlarmManager, services and receivers sit behind seams in its `android/`, `receiver/` and `service/` subpackages. |
|
||||
| Screens | `ui/<area>/` | One ViewModel + immutable `UiState` per area; Compose for the screen. |
|
||||
|
||||
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) §3 lists every package.
|
||||
|
||||
## Code style & conventions
|
||||
|
||||
- **Kotlin**, 4-space indent, LF line endings, final newline, no trailing
|
||||
whitespace — all enforced by `.editorconfig` (2-space for yaml/toml/json/md).
|
||||
whitespace, all enforced by `.editorconfig` (2-space for yaml/toml/json/md).
|
||||
Match the surrounding code.
|
||||
- **Material 3 Expressive** for all UI: use `MaterialExpressiveTheme`, the
|
||||
colour-scheme tokens (never hardcoded colours), and canonical M3 components
|
||||
(e.g. `ListItem` for rows). Consult the `material-3` skill before designing a
|
||||
new screen or component.
|
||||
- Prefer the domain layer for anything testable; keep `AndroidTasksDataSource`
|
||||
the only Android-coupled data implementation so the rest stays JVM-testable.
|
||||
- Prefer the domain layer for anything testable; keep Android APIs behind the
|
||||
seams in each engine's `android/` subpackage so the rest stays JVM-testable.
|
||||
|
||||
## Tests
|
||||
|
||||
- New domain logic (mappers, filters, sorting, forms, value mapping) **must**
|
||||
come with JVM unit tests under `app/src/test/`. The data source is the
|
||||
JVM-testable seam — mock or fake it rather than reaching for instrumentation.
|
||||
- Add an instrumented test only when a path genuinely needs a real
|
||||
`ContentResolver`.
|
||||
- New domain logic (scheduling, ring policies, formatting, mappers) **must**
|
||||
come with JVM unit tests under `app/src/test/`. Repositories and engine seams
|
||||
are the JVM-testable boundary: fake them rather than reaching for
|
||||
instrumentation.
|
||||
- Add an instrumented test only when a path genuinely needs a real device
|
||||
(Room migrations, AlarmManager, a foreground service).
|
||||
|
||||
## Commits & PRs
|
||||
|
||||
- Write focused commits with clear messages (the existing history uses short,
|
||||
scoped subjects like `UI: ...` / `M1 ...`).
|
||||
conventional subjects like `feat(widgets): ...` / `fix(ringtones): ...`).
|
||||
- Update [`CHANGELOG.md`](CHANGELOG.md) under `[Unreleased]` for any
|
||||
user-visible change — its sections feed the release notes and F-Droid "What's
|
||||
user-visible change. Its sections feed the release notes and F-Droid "What's
|
||||
New" (see [`docs/RELEASING.md`](docs/RELEASING.md)).
|
||||
- If your change shifts the architecture or completes a milestone, update
|
||||
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) / [`docs/ROADMAP.md`](docs/ROADMAP.md)
|
||||
in the same PR.
|
||||
- Don't bump `versionName` / `versionCode` in a regular PR — the committed
|
||||
- Don't bump `versionName` / `versionCode` in a regular PR. The committed
|
||||
`versionName` is bumped only when **cutting a release** (that bump reaching
|
||||
`main` is what triggers the release; the pipeline then mints the tag). See
|
||||
[`docs/RELEASING.md`](docs/RELEASING.md).
|
||||
|
||||
## Scope
|
||||
|
||||
v1 is four surfaces — alarms, timers, stopwatch, world clock — done properly,
|
||||
v1 is four surfaces (alarms, timers, stopwatch, world clock) done properly,
|
||||
and nothing else. Widgets, a Quick Settings tile, screensaver mode and a bedtime
|
||||
schedule are all wanted, and all deliberately deferred until v1 is finished; see
|
||||
[`docs/ROADMAP.md`](docs/ROADMAP.md).
|
||||
|
||||
Two things are out of scope by design. **A sync stack of our own** — your alarms
|
||||
Two things are out of scope by design. **A sync stack of our own**: your alarms
|
||||
export to a documented JSON file, and syncing them is a job for something that
|
||||
already does syncing well. **Anything that makes an alarm less reliable in
|
||||
exchange for a feature** — if a change trades away the certainty that an alarm
|
||||
exchange for a feature**: if a change trades away the certainty that an alarm
|
||||
rings, it loses, however nice the feature is.
|
||||
|
||||
## License
|
||||
|
||||
+393
-395
File diff suppressed because it is too large.
Load diff
+14
-14
@@ -1,6 +1,6 @@
|
||||
# Clockula backup format
|
||||
|
||||
`clockula-backup-v1.json` — a documented, versioned JSON export of Clockula's
|
||||
`clockula-backup-v1.json` is a documented, versioned JSON export of Clockula's
|
||||
own data, written to (and read from) a user-chosen folder via SAF
|
||||
(`docs/PLAN.md` §7). This file is the schema's source of truth; the codec
|
||||
(`data/backup/BackupDto.kt`, `BackupCodec.kt`) implements exactly what is
|
||||
@@ -20,18 +20,18 @@ written here.
|
||||
}
|
||||
```
|
||||
|
||||
- `format` — must be exactly `"clockula-backup"`.
|
||||
- `version` — must be exactly `1`. Any other value rejects the whole file:
|
||||
- `format`: must be exactly `"clockula-backup"`.
|
||||
- `version`: must be exactly `1`. Any other value rejects the whole file:
|
||||
Clockula does not guess at a schema it has not implemented.
|
||||
- `exportedAt` — epoch milliseconds, informational only (not re-imported).
|
||||
- `exportedAt`: epoch milliseconds, informational only (not re-imported).
|
||||
|
||||
## Parsing rules
|
||||
|
||||
- **Tolerant of unknown fields, at any level.** An older or newer client's
|
||||
extra field is ignored, never an error.
|
||||
- **Strict about known fields.** A malformed value in a field this schema
|
||||
defines — `format`, `version`, an alarm's `hour`/`minute`, a world clock's
|
||||
`zone_id`, a timer's `duration_millis` — rejects the **whole file** with a
|
||||
defines (`format`, `version`, an alarm's `hour`/`minute`, a world clock's
|
||||
`zone_id`, a timer's `duration_millis`) rejects the **whole file** with a
|
||||
specific reason. Nothing is imported half-way.
|
||||
- Settings values (snooze minutes, volume ramp, …) are
|
||||
**not** range-checked at parse time; they pass through the same clamps
|
||||
@@ -41,19 +41,19 @@ written here.
|
||||
- An enum field with a name this version does not recognise (a `dismiss_
|
||||
challenge` or `theme_mode` from a future version, say) degrades to that
|
||||
field's default, the same forgiving-read posture `AlarmMapper` already uses
|
||||
for a corrupt Room row — it does not fail the import.
|
||||
for a corrupt Room row. It does not fail the import.
|
||||
|
||||
## Import semantics
|
||||
|
||||
Import **replaces all state** — alarms, timers and world clocks are deleted
|
||||
Import **replaces all state**: alarms, timers and world clocks are deleted
|
||||
and re-created from the file, and every setting the file carries is written
|
||||
over the current value. This is not a merge, and the backup screen says so
|
||||
before the user confirms. A ringing alarm is dismissed and any running timer
|
||||
is deleted *before* the replace, so the import never leaves an orphaned ring
|
||||
session; alarm/timer/stopwatch scheduling is re-resolved immediately after.
|
||||
|
||||
Every imported alarm and timer gets a **fresh id and fresh timestamps** —
|
||||
the backup carries no id and no `created_at`/`updated_at`, since those are
|
||||
Every imported alarm and timer gets a **fresh id and fresh timestamps**.
|
||||
The backup carries no id and no `created_at`/`updated_at`, since those are
|
||||
local-database facts, not portable ones.
|
||||
|
||||
## Alarms
|
||||
@@ -77,7 +77,7 @@ One object per row of `alarms`, config only:
|
||||
| `delete_after_use` | Boolean | A transient, contract-created alarm. |
|
||||
|
||||
**Not exported:** the `alarm_states` ring-state table (snooze/ringing/handled-
|
||||
occurrence bookkeeping) — it is volatile, per-device state, not configuration,
|
||||
occurrence bookkeeping). It is volatile, per-device state, not configuration,
|
||||
and `docs/ARCHITECTURE.md` §4/§6 are explicit that it must never appear here.
|
||||
|
||||
## Timers
|
||||
@@ -93,7 +93,7 @@ One object per row of `timers`, config only:
|
||||
| `delete_after_use` | Boolean | |
|
||||
|
||||
**Not exported:** `state`, `remaining`, the elapsed-realtime anchors, or
|
||||
`ends_at_wall_clock` — a timer's running state is meaningless on another
|
||||
`ends_at_wall_clock`. A timer's running state is meaningless on another
|
||||
device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
|
||||
`IDLE`, with `remaining` equal to `duration`.
|
||||
|
||||
@@ -128,12 +128,12 @@ files may also carry `default_timer_duration_millis`, from a default timer
|
||||
length setting that has since been removed; it is ignored on import like any
|
||||
other unknown field. Every
|
||||
numeric/duration value is clamped on *apply*, through the existing pref
|
||||
setters — never on parse.
|
||||
setters, never on parse.
|
||||
|
||||
## Ringtone URIs
|
||||
|
||||
Exported exactly as stored (a `content://` URI, typically). On import the
|
||||
URI is written back unchanged; if the *importing* device cannot resolve it
|
||||
at ring/preview time, the existing ringtone-resolution fallback already used
|
||||
elsewhere in the app applies (falls through to the device default) — this is
|
||||
elsewhere in the app applies (falls through to the device default). This is
|
||||
not special-cased in the backup path itself.
|
||||
+34
-34
@@ -1,6 +1,6 @@
|
||||
# Clockula — implementation plan
|
||||
|
||||
> The **design decisions and their rationale** — the "why". Locked unless
|
||||
> The design decisions and their rationale (the "why"). Locked unless
|
||||
> explicitly overturned here. For the moving status view see
|
||||
> [`ROADMAP.md`](ROADMAP.md); for the shape of the code as it actually stands,
|
||||
> [`ARCHITECTURE.md`](ARCHITECTURE.md) (written from M2 onward).
|
||||
@@ -32,7 +32,7 @@ in three concrete places:
|
||||
1. **The platform alarm contract.** Clockula implements `android.provider.AlarmClock`
|
||||
in full (§6), so Assistant, automation apps and anything else that speaks the
|
||||
standard intent vocabulary can drive it. That contract *is* the open interface
|
||||
for clocks on Android — the same role `CalendarContract` plays for Calendula.
|
||||
for clocks on Android, the same role `CalendarContract` plays for Calendula.
|
||||
Being a good citizen of it is the thesis, applied.
|
||||
2. **IANA tzdata.** World clocks are IANA zone IDs, not a bespoke city table (§5).
|
||||
3. **Your data leaves whenever you want.** A documented, versioned JSON backup
|
||||
@@ -44,7 +44,7 @@ Clockula owns storage, but **nothing above the data layer knows Room exists**.
|
||||
The app talks to `AlarmRepository` / `TimerRepository` / `WorldClockRepository` /
|
||||
`StopwatchRepository`; only the DAOs and one data source per aggregate touch Room
|
||||
entities. Domain models are plain Kotlin. **Never let a Room entity or a
|
||||
`@Query` string leak above the data layer** — the same discipline that let
|
||||
`@Query` string leak above the data layer**. That is the same discipline that let
|
||||
Agendula swap its whole storage posture without touching the UI.
|
||||
|
||||
---
|
||||
@@ -89,7 +89,7 @@ recurrence) are a different animal from a due-date nudge.
|
||||
|
||||
## 3. floret-kit
|
||||
|
||||
Consumed as a git submodule + Gradle composite build, as the siblings do —
|
||||
Consumed as a git submodule + Gradle composite build, as the siblings do:
|
||||
`includeBuild("floret-kit")`, `implementation("de.jeanlucmakiola.floret:<module>")`,
|
||||
with a gitignored `clockula/floret-kit/local.properties` locally and
|
||||
`ANDROID_HOME` in CI.
|
||||
@@ -128,7 +128,7 @@ Everything else in this app is a readout. This is the engineering.
|
||||
`TIME_SET`, `TIMEZONE_CHANGED`, and `MY_PACKAGE_REPLACED`. DST transitions are
|
||||
handled by this re-resolution, not by arithmetic on stored millis. A 02:30
|
||||
alarm on a spring-forward night has a defined, tested behaviour.
|
||||
- Scheduled with `AlarmManager.setAlarmClock()` — not `setExactAndAllowWhileIdle`.
|
||||
- Scheduled with `AlarmManager.setAlarmClock()`, not `setExactAndAllowWhileIdle`.
|
||||
`setAlarmClock` is the only variant the system treats as a user-visible alarm:
|
||||
it is doze-exempt, and it populates `getNextAlarmClock()`, which is what draws
|
||||
the status-bar alarm icon and the lockscreen line. Permission is
|
||||
@@ -147,7 +147,7 @@ notification whose content intent is the ring activity.
|
||||
- Full-screen intent on Android 14+ needs `USE_FULL_SCREEN_INTENT`; check
|
||||
`canUseFullScreenIntent()` and deep-link to the grant screen when denied. A
|
||||
denied FSI must degrade to a high-priority heads-up notification that still
|
||||
rings — never to silence.
|
||||
rings, never to silence.
|
||||
- Gradual volume ramp, per-alarm ringtone (system `RingtoneManager` URIs, so the
|
||||
user's existing sounds work), vibration pattern, `AudioAttributes` with
|
||||
`USAGE_ALARM` so DND's alarm exemption applies.
|
||||
@@ -162,7 +162,7 @@ notification whose content intent is the ring activity.
|
||||
A "**Why might my alarm not ring?**" screen that reports actual device state, not
|
||||
generic advice: exact-alarm permission, notification permission, full-screen-intent
|
||||
permission, battery-optimisation exemption, **whether DND currently blocks alarms**
|
||||
(read via `NotificationManager.getCurrentInterruptionFilter()` — Clockula never
|
||||
(read via `NotificationManager.getCurrentInterruptionFilter()`; Clockula never
|
||||
requests `ACCESS_NOTIFICATION_POLICY` or changes DND itself, it only reports what the
|
||||
user already has), whether the OEM has Clockula under an aggressive app-killer, and
|
||||
the next scheduled fire time as the system sees it (`getNextAlarmClock()`), compared
|
||||
@@ -175,19 +175,19 @@ the exact settings page. This screen is a v1 feature, not a support afterthought
|
||||
|
||||
Room, schema exported and version-controlled, migrations tested from v1.
|
||||
|
||||
- **`alarms`** — id, hour, minute, label, enabled, repeat-day mask, ringtone URI,
|
||||
- **`alarms`**: id, hour, minute, label, enabled, repeat-day mask, ringtone URI,
|
||||
vibrate, snooze minutes, snooze limit, volume-ramp seconds, dismiss challenge,
|
||||
`skipNextOccurrence`, timestamps.
|
||||
- **`timers`** — id, label, duration, state (idle/running/paused/expired), the
|
||||
- **`timers`**: id, label, duration, state (idle/running/paused/expired), the
|
||||
elapsed-realtime anchor it resolves against, ringtone, sort order. Multiple
|
||||
concurrent timers are first-class.
|
||||
- **`world_clocks`** — id, IANA zone ID, optional custom label, sort order.
|
||||
- **`stopwatch_laps`** — id, index, split, cumulative. The stopwatch's running
|
||||
- **`world_clocks`**: id, IANA zone ID, optional custom label, sort order.
|
||||
- **`stopwatch_laps`**: id, index, split, cumulative. The stopwatch's running
|
||||
state lives in DataStore (a single record), its laps in Room so they survive
|
||||
process death.
|
||||
|
||||
Running timers and the stopwatch anchor on **`SystemClock.elapsedRealtime()`**,
|
||||
never wall-clock — a user changing the time must not warp a running timer. Alarms
|
||||
never wall-clock: a user changing the time must not warp a running timer. Alarms
|
||||
are the opposite: wall-clock by definition. This distinction is the single
|
||||
easiest thing to get wrong in a clock app; it is tested both ways.
|
||||
|
||||
@@ -226,12 +226,12 @@ than writing anything. `SKIP_UI` never means "skip validation".
|
||||
"timers": […], "worldClocks": […], "settings": {…} }`. The schema is **documented
|
||||
in the repo** (`docs/BACKUP.md`), versioned, and import is tolerant of unknown
|
||||
fields (ignored, at any level) and strict about known ones (a malformed known
|
||||
field rejects the whole file — nothing is imported half-way). Ringtone URIs are
|
||||
field rejects the whole file, so nothing is imported half-way). Ringtone URIs are
|
||||
exported as-is with a documented fallback when the target device can't resolve
|
||||
them.
|
||||
|
||||
Import is **whole-state replacement**, not a merge: the user is shown exactly
|
||||
that before confirming. Alarm and timer records carry only configuration — ring
|
||||
that before confirming. Alarm and timer records carry only configuration. Ring
|
||||
state (`alarm_states`, a timer's running anchors) is never exported and never
|
||||
touched by an import; an imported timer always lands `IDLE`. Record field names
|
||||
mirror the Room column vocabulary (§5), so the schema, the exported Room schema
|
||||
@@ -241,7 +241,7 @@ and the backup all read the same words.
|
||||
|
||||
## 8. Identity
|
||||
|
||||
Seed **`#6B7A5C`** — a muted sage/olive.
|
||||
Seed **`#6B7A5C`**, a muted sage/olive.
|
||||
|
||||
The family's seeds are one colour rotated: Calendula `#5C6B7A` (slate blue) →
|
||||
Agendula `#7A5C6B` (warm mauve) → Clockula `#6B7A5C` (sage). Same three bytes,
|
||||
@@ -254,10 +254,10 @@ fallback, hand-tuned light/dark `ColorScheme`s in `ui/theme/Color.kt`.
|
||||
|
||||
Design principles inherited from the family (non-negotiable):
|
||||
- **Expressive ≠ big or bold.** Refinement comes from shape, colour, space and
|
||||
motion — never from enlarging text or upping weight.
|
||||
motion, never from enlarging text or upping weight.
|
||||
- Content lives **on surfaces**, not as bare text on the background.
|
||||
- `MaterialShapes` / `androidx.graphics.shapes` morphs are for the genuinely
|
||||
clock-specific surfaces — the analog face, the ring screen, timer progress —
|
||||
clock-specific surfaces (the analog face, the ring screen, timer progress),
|
||||
not sprinkled everywhere.
|
||||
|
||||
A clock app is mostly one enormous number. That readout is where the design
|
||||
@@ -267,24 +267,24 @@ lives, and it is the thing to get right before anything else.
|
||||
|
||||
## 9. Navigation
|
||||
|
||||
Four tabs — Alarm, Timer, Stopwatch, World clock — in an M3 navigation bar, with
|
||||
Four tabs (Alarm, Timer, Stopwatch, World clock) in an M3 navigation bar, with
|
||||
a **live pill** floating above it whenever a timer or the stopwatch is running:
|
||||
current value plus pause and stop, reachable from any tab.
|
||||
|
||||
This is the one deliberate departure from the stock model, and it exists because
|
||||
the stock clock's worst flaw is that a running timer is invisible unless you are
|
||||
standing on its tab. The pill is not decoration; it is the fix.
|
||||
standing on its tab.
|
||||
|
||||
Adaptive: navigation rail on wide layouts, per M3 adaptive guidance.
|
||||
|
||||
### Home-screen widgets (M10)
|
||||
|
||||
Three Jetpack Glance app widgets, each a read-only mirror of state that already
|
||||
exists — no widget owns data: **next alarm** (day, time, label), **timer**
|
||||
exists (no widget owns data): **next alarm** (day, time, label), **timer**
|
||||
(the live pill's subject timer, same selection logic, with pause/resume/reset),
|
||||
and **clock** (local time, growing to world clocks at larger sizes). Tapping a
|
||||
widget opens the matching tab through the same `MainActivity` intents the
|
||||
`AlarmClock` contract (§6) already uses — a widget is just another caller of
|
||||
`AlarmClock` contract (§6) already uses. A widget is just another caller of
|
||||
that door, not a second entry point. Refresh is event-driven (`updatePeriodMillis
|
||||
= 0`): widgets update when the state they mirror changes, not on a timer.
|
||||
|
||||
@@ -292,24 +292,24 @@ that door, not a second entry point. Refresh is event-driven (`updatePeriodMilli
|
||||
|
||||
## 10. Repository & pipeline
|
||||
|
||||
**Codeberg is canonical from the first commit** — git, issues, PRs and releases.
|
||||
**Codeberg is canonical from the first commit**: git, issues, PRs and releases.
|
||||
The self-hosted Gitea instance holds the secrets and runs the release job, and
|
||||
push-mirrors *from* Codeberg. There is no Gitea-canonical phase to migrate out
|
||||
of, which is the mistake Agendula had to unwind.
|
||||
|
||||
The pipeline is copied wholesale from the siblings:
|
||||
|
||||
- `.forgejo/workflows/ci.yaml` — contributor-facing, no secrets. Copy Agendula's,
|
||||
- `.forgejo/workflows/ci.yaml`: contributor-facing, no secrets. Copy Agendula's,
|
||||
adjusting `SKIP_RE`.
|
||||
- `.forgejo/workflows/translations.yaml` — identical in both siblings; copy as-is.
|
||||
- `.gitea/workflows/release.yaml` — **Agendula's spine** (Codeberg-canonical: pushes
|
||||
- `.forgejo/workflows/translations.yaml`: identical in both siblings; copy as-is.
|
||||
- `.gitea/workflows/release.yaml`: **Agendula's spine** (Codeberg-canonical: pushes
|
||||
the tag to Codeberg itself, pre-1.0 prerelease flag, and the Codeberg publish
|
||||
step is *not* `continue-on-error`, after five Calendula-era releases reported
|
||||
green while never publishing) **plus Calendula's `play` job** grafted on
|
||||
unchanged — last, isolated, `continue-on-error`, skipping cleanly until
|
||||
unchanged. It runs last, isolated and `continue-on-error`, and skips cleanly until
|
||||
`PLAY_SERVICE_ACCOUNT_JSON` exists. It therefore stays dormant through the
|
||||
pre-1.0 run, which is the correct behaviour anyway.
|
||||
- `.gitea/workflows/renovate.yml` — Agendula's, including the
|
||||
- `.gitea/workflows/renovate.yml`: Agendula's, including the
|
||||
`github.repository_owner == 'makiolaj'` guard.
|
||||
- Issue templates, PR template, F-Droid metadata, fastlane tree.
|
||||
|
||||
@@ -331,8 +331,8 @@ useful reference for the parts of an alarm app that are tedious to rediscover.
|
||||
- **Default posture: read it, don't copy it.** Understanding how it schedules and
|
||||
how its services are wired is free; retyping our own implementation from that
|
||||
understanding keeps Clockula cleanly MIT.
|
||||
- **Where we genuinely lift code** — realistically only intricate scheduling or
|
||||
service-lifecycle edge cases — that file keeps its Apache-2.0 header and
|
||||
- **Where we genuinely lift code** (realistically only intricate scheduling or
|
||||
service-lifecycle edge cases), that file keeps its Apache-2.0 header and
|
||||
attribution, is listed in `docs/PROVENANCE.md`, and the Apache text ships in
|
||||
`licenses/`. This is exactly the pattern Agendula already uses for the vendored
|
||||
dmfs provider, so it is a known, working arrangement rather than a new risk.
|
||||
@@ -348,16 +348,16 @@ DAO/repository, and the Room entity shapes as a sanity check on §5.
|
||||
- Its **always-on foreground service** (`AlarmAlwaysForegroundService`). Keeping a
|
||||
permanent service alive to make alarms reliable is a battery-cost workaround;
|
||||
`setAlarmClock` is the supported mechanism and does not need it.
|
||||
- Its **Pomodoro** feature — out of scope.
|
||||
- Its **Pomodoro** feature, which is out of scope.
|
||||
- Its **settings/tile component vocabulary** (`ui/components/tiles/*`,
|
||||
`SettingTile`) — floret-kit's `GroupedRow` / `OptionCard` / `OptionPicker` are
|
||||
`SettingTile`). floret-kit's `GroupedRow` / `OptionCard` / `OptionPicker` are
|
||||
our equivalents and keep the family coherent.
|
||||
- **MaterialKolor + a colour-picker dependency** — the kit's `identity` module
|
||||
- **MaterialKolor + a colour-picker dependency**. The kit's `identity` module
|
||||
owns theming.
|
||||
- Its **no-Hilt, manual-ViewModel structure** and `minSdk 24`.
|
||||
|
||||
For interaction detail — snooze behaviour, the ring screen, timer presentation,
|
||||
what the stopwatch does with laps — **Google Clock is the reference**, not
|
||||
For interaction detail (snooze behaviour, the ring screen, timer presentation,
|
||||
what the stopwatch does with laps), **Google Clock is the reference**, not
|
||||
ClockMaster.
|
||||
|
||||
---
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
# Clockula — documentation
|
||||
|
||||
Clockula is a Material 3 Expressive **clock** app for Android — alarms, timers,
|
||||
Clockula is a Material 3 Expressive **clock** app for Android: alarms, timers,
|
||||
stopwatch and world clock. Third floret of the bloom, after
|
||||
[Calendula](https://codeberg.org/jlmakiola/calendula) (calendar) and
|
||||
[Agendula](https://codeberg.org/jlmakiola/agendula) (tasks). See the top-level
|
||||
[`../README.md`](../README.md) for the pitch.
|
||||
|
||||
Unlike its siblings, Clockula owns its storage — there is no open provider behind
|
||||
Unlike its siblings, Clockula owns its storage, because there is no open provider behind
|
||||
a clock. What that costs and how the open-standards commitment is honoured anyway
|
||||
is the first section of [`PLAN.md`](PLAN.md).
|
||||
|
||||
@@ -15,8 +15,8 @@ is the first section of [`PLAN.md`](PLAN.md).
|
||||
| Doc | What it covers |
|
||||
|---|---|
|
||||
| [`PLAN.md`](PLAN.md) | **The spec.** Thesis, locked decisions, the alarm engine, data model, system interop, identity, pipeline, and how ClockMaster is used. The "why". |
|
||||
| [`ROADMAP.md`](ROADMAP.md) | **Status and the work queue** — milestones M0–M11 and post-v1. What the build loop works through. |
|
||||
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How Clockula is built **today** — layers, the data seam, the alarm engine, DI, build/tooling, manifest. Written from M2 onward. |
|
||||
| [`ROADMAP.md`](ROADMAP.md) | **Status and the work queue**: milestones M0–M11 and post-v1. What the build loop works through. |
|
||||
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How Clockula is built **today**: layers, the data seam, the alarm engine, DI, build/tooling, manifest. Written from M2 onward. |
|
||||
| [`RELEASING.md`](RELEASING.md) | How to cut a release. Written at M0 alongside the pipeline. |
|
||||
| [`PROVENANCE.md`](PROVENANCE.md) | Any third-party code carried in-tree and under what licence. Created only if we actually lift something (`PLAN.md` §11). |
|
||||
|
||||
|
||||
+207
-207
@@ -1,6 +1,6 @@
|
||||
# Clockula — roadmap
|
||||
|
||||
The **status** view, and the work queue. Design rationale for every decision here
|
||||
The status view and the work queue. Design rationale for every decision here
|
||||
lives in [`PLAN.md`](PLAN.md); the shape of the code as it stands will live in
|
||||
[`ARCHITECTURE.md`](ARCHITECTURE.md) from M2 onward.
|
||||
|
||||
@@ -13,16 +13,16 @@ Status legend: ✅ done · 🚧 in progress · ⬜ not started
|
||||
✅ **M10 done.** Clockula now has a settings hub (theme, dynamic colour,
|
||||
language, the alarm/timer/clock defaults every new alarm or timer inherits,
|
||||
help), a documented, versioned JSON backup (`docs/BACKUP.md`) that exports and
|
||||
restores alarms/timers/world clocks and settings through SAF — configuration
|
||||
only, never ring state, never a timer's running anchors — as a whole-state
|
||||
restores alarms/timers/world clocks and settings through SAF (configuration
|
||||
only, never ring state, never a timer's running anchors) as a whole-state
|
||||
replace the screen is explicit about before the user confirms, a "why might my
|
||||
alarm not ring?" self-check that reads real device state (exact-alarm and
|
||||
notification permissions, full-screen intent, battery optimisation, DND, alarm
|
||||
volume, a known aggressive-OEM allowlist, and the system's own next alarm
|
||||
compared against what Clockula computed) and deep-links to the exact settings
|
||||
page for each, and three Glance home-screen widgets — next alarm, timer,
|
||||
clock — that mirror existing state and redraw event-driven, on every write,
|
||||
with no ticker of their own. **M11** (release readiness) is next.
|
||||
page for each, and three Glance home-screen widgets (next alarm, timer,
|
||||
clock) that mirror existing state and redraw event-driven, on every write,
|
||||
with no ticker of their own. M11 (release readiness) is next.
|
||||
|
||||
---
|
||||
|
||||
@@ -30,12 +30,12 @@ with no ticker of their own. **M11** (release readiness) is next.
|
||||
|
||||
One milestone per iteration, auto-continuing. Each iteration:
|
||||
|
||||
1. Pick the **first milestone not marked ✅**.
|
||||
2. Implement it **completely** — every checklist item under it.
|
||||
1. Pick the first milestone not marked ✅.
|
||||
2. Implement it completely: every checklist item under it.
|
||||
3. Build, and run the tests the milestone names. A milestone is not done until
|
||||
they pass.
|
||||
4. **Commit** (floret-kit submodule first where it changed, then Clockula's
|
||||
pointer bump — separate commits, granular, lowercase area prefix).
|
||||
pointer bump; separate commits, granular, lowercase area prefix).
|
||||
5. Mark the milestone ✅ here, update **Current state**, and commit that too.
|
||||
6. Continue to the next milestone.
|
||||
|
||||
@@ -45,7 +45,7 @@ Rules that hold across every iteration:
|
||||
(`PLAN.md` §0).
|
||||
- **Never** add a foojay toolchain resolver, anywhere, not even in a comment.
|
||||
- Commit each finished unit of work; never accumulate a WIP pile.
|
||||
- If a milestone turns out to be wrong, say so and amend `PLAN.md` — do not
|
||||
- If a milestone turns out to be wrong, say so and amend `PLAN.md`. Do not
|
||||
silently build something else.
|
||||
- `ARCHITECTURE.md` and `CHANGELOG.md` are updated as part of the milestone that
|
||||
changes them, not in a later cleanup pass.
|
||||
@@ -75,7 +75,7 @@ The project exists, builds, is themed, and ships itself.
|
||||
`scripts/check_reproducible_release.sh` reports every invariant holding.
|
||||
Two notes for later: the store listing and `fastlane` changelogs are written
|
||||
but no screenshots exist yet (M11), and `docs/ARCHITECTURE.md` is still
|
||||
unwritten by design — M2 writes it, once there is an architecture to
|
||||
unwritten by design. M2 writes it, once there is an architecture to
|
||||
describe.
|
||||
|
||||
### ✅ M1 — floret-kit: `core-prefs` + `core-di`
|
||||
@@ -83,16 +83,16 @@ Pay the extraction the kit's roadmap has been waiting for a third app to trigger
|
||||
- In the kit: `core-prefs` (`ThemeMode`, `dynamicColor`, typed DataStore wrapper,
|
||||
`toEnum()`) and `core-di` (`@IoDispatcher` + provider), each with tests, module
|
||||
docs, `CHANGELOG` and `ROADMAP`/`ARCHITECTURE` updates.
|
||||
- Clockula consumes both. Migrating Calendula and Agendula is explicitly **out of
|
||||
scope** — note it in the kit's roadmap as a follow-up.
|
||||
- Clockula consumes both. Migrating Calendula and Agendula is explicitly out of
|
||||
scope; note it in the kit's roadmap as a follow-up.
|
||||
- **Done when:** the kit's tests pass, Clockula builds against the new modules,
|
||||
and both repos are committed (submodule first, then the pointer bump).
|
||||
|
||||
✅ floret-kit 0.4.0. `core-prefs` (56 kit tests) gives the family a typed
|
||||
`Pref<T>`/`PrefStore` over DataStore; reads degrade to the default instead of
|
||||
throwing, and `flow()` is distinct-until-changed. The appearance key names are
|
||||
frozen at what Calendula and Agendula already write, so the sibling migration —
|
||||
a follow-up in the kit's roadmap, not here — needs no data migration.
|
||||
frozen at what Calendula and Agendula already write, so the sibling migration
|
||||
(a follow-up in the kit's roadmap, not here) needs no data migration.
|
||||
`core-di` carries `hilt-compiler` itself, since an `@InstallIn` module only
|
||||
reaches a consumer's component if its own library was processed; Clockula's
|
||||
`assembleDebug` is the integration test for that binding. Clockula's theme now
|
||||
@@ -114,19 +114,19 @@ The whole storage stack, headless. No UI.
|
||||
|
||||
✅ Four tables (`alarms`, `timers`, `world_clocks`, `stopwatch_laps`), schema
|
||||
v1 exported to `app/schemas` so migrations can be tested from the beginning.
|
||||
The seam that matters: the app talks to four repository interfaces speaking
|
||||
domain types and Flows, and `ArchitectureRulesTest` fails the build if Room, an
|
||||
entity or a query string ever appears above `data/`. Time is a parameter rather
|
||||
than an ambient read — `WallClock` and `ElapsedRealtimeClock` are distinct
|
||||
types, so a running timer anchors on the monotonic clock and keeps a wall-clock
|
||||
value only as a post-reboot fallback, and the stopwatch keeps none at all. That
|
||||
leaves one honest gap, written down rather than glossed: once a new boot's
|
||||
uptime climbs past a stored anchor the reboot goes unnoticed, so closing it
|
||||
needs a persisted boot id and M3's `BOOT_COMPLETED` receiver. Every
|
||||
read-modify-write runs inside a DAO transaction, and the stopwatch run record
|
||||
is written whole in one DataStore edit, so concurrent writers cannot swallow
|
||||
each other's edits or persist a torn record. 151 JVM tests over fake DAOs and
|
||||
injected fake clocks; the migration test is the only thing needing a device.
|
||||
The app talks to four repository interfaces speaking domain types and Flows,
|
||||
and `ArchitectureRulesTest` fails the build if Room, an entity or a query
|
||||
string ever appears above `data/`. Time is a parameter rather than an ambient
|
||||
read: `WallClock` and `ElapsedRealtimeClock` are distinct types, so a running
|
||||
timer anchors on the monotonic clock and keeps a wall-clock value only as a
|
||||
post-reboot fallback, and the stopwatch keeps none at all. That leaves one
|
||||
gap: once a new boot's uptime climbs past a stored anchor the reboot goes
|
||||
unnoticed, so closing it needs a persisted boot id and M3's `BOOT_COMPLETED`
|
||||
receiver. Every read-modify-write runs inside a DAO transaction, and the
|
||||
stopwatch run record is written whole in one DataStore edit, so concurrent
|
||||
writers cannot swallow each other's edits or persist a torn record. 151 JVM
|
||||
tests over fake DAOs and injected fake clocks; the migration test is the only
|
||||
thing needing a device.
|
||||
|
||||
### ✅ M3 — Alarm engine
|
||||
The hard part (`PLAN.md` §4), still headless apart from the ring screen's
|
||||
@@ -140,13 +140,13 @@ plumbing.
|
||||
- Ringing foreground service: audio focus, `USAGE_ALARM`, volume ramp, vibration,
|
||||
ringtone URIs; survives process death and reboot mid-ring.
|
||||
- Full-screen-intent notification + `canUseFullScreenIntent()` handling, degrading
|
||||
to a high-priority heads-up notification that still rings — never to silence.
|
||||
to a high-priority heads-up notification that still rings, never to silence.
|
||||
- **Tests:** DST spring-forward and fall-back, every repeat configuration, skip,
|
||||
snooze-vs-next-occurrence. These are the tests the app lives or dies on.
|
||||
|
||||
✅ 326 JVM tests, 172 of them new. The engine is a plain Kotlin class that
|
||||
reaches Android through four interfaces it does not implement, so the
|
||||
behaviour the app lives or dies on is pinned without an emulator —
|
||||
behaviour the app lives or dies on is pinned without an emulator.
|
||||
`ArchitectureRulesTest` fails the build if `android.*` ever appears in
|
||||
`domain/`, `AlarmEngine.kt` or `system/`. Occurrences are generated by walking
|
||||
local dates through `ZonedDateTime.of` rather than by adding 24 hours to an
|
||||
@@ -155,43 +155,43 @@ plumbing.
|
||||
fall-back night. Asserted for Berlin and New York and across all 128 repeat
|
||||
masks. Two rules keep constant re-resolution honest: a two-minute grace window
|
||||
so a late fire still counts, and a `handled_occurrence` watermark that can
|
||||
suppress a re-fire but — because suppression also requires the candidate to be
|
||||
at-or-before now — can never silence the future. Skip needed a watermark of its
|
||||
suppress a re-fire but can never silence the future, because suppression also
|
||||
requires the candidate to be at-or-before now. Skip needed a watermark of its
|
||||
own, since a bare flag eats a second alarm once the skipped occurrence passes.
|
||||
At most one alarm rings and a newly-firing one takes over, so closing a cycle
|
||||
touches the shared ring and auto-silence slots only when that alarm is the one
|
||||
ringing; every write to `alarm_states` runs under one mutex, because a cold
|
||||
start triggered by the fire broadcast otherwise races its own reschedule pass.
|
||||
The "never to silence" chain is a testable invariant, not a hope: full-screen
|
||||
intent, else a heads-up notification that still rings, else the default
|
||||
ringtone, else forced vibration. Schema v2 adds `alarm_states`, and the boot id
|
||||
closes M2's blind spot for running timers **and** the stopwatch — a scope move
|
||||
from M7, recorded in `ARCHITECTURE.md` §5. Knowingly open: the migration and
|
||||
the other 12 instrumentation tests compile but have never run on hardware, as
|
||||
no device was attached; an auto-silenced alarm is silently missed, with no
|
||||
missed-alarm notification until someone asks for one.
|
||||
The "never to silence" chain is a testable invariant: full-screen intent, else
|
||||
a heads-up notification that still rings, else the default ringtone, else
|
||||
forced vibration. Schema v2 adds `alarm_states`, and the boot id closes M2's
|
||||
blind spot for running timers and the stopwatch, a scope move from M7
|
||||
recorded in `ARCHITECTURE.md` §5. Knowingly open: the migration and the other
|
||||
12 instrumentation tests compile but have never run on hardware, as no device
|
||||
was attached; an auto-silenced alarm is silently missed, with no missed-alarm
|
||||
notification until someone asks for one.
|
||||
|
||||
### ✅ M4 — App shell
|
||||
- Navigation host, four tabs, M3 navigation bar, adaptive rail on wide layouts.
|
||||
- The **live pill** (`PLAN.md` §9) — shared running-state surface, pause + stop
|
||||
- The **live pill** (`PLAN.md` §9): shared running-state surface, pause + stop
|
||||
from any tab.
|
||||
- The ring screen UI: over-lockscreen, turn-screen-on, snooze/dismiss, optional
|
||||
dismiss challenge that cannot strand the user.
|
||||
- Motion from the kit's `identity` module; predictive back.
|
||||
|
||||
✅ 480 JVM tests, 154 of them new, and still **no Robolectric** — which had to
|
||||
be earned rather than kept. Everything that decides something moved out of the
|
||||
composables: `ShellNavigation` (what a tab tap does to the back stack, and
|
||||
that back from a tab goes to Alarms while back from Alarms leaves the app),
|
||||
✅ 480 JVM tests, 154 of them new, and still no Robolectric. Keeping it that
|
||||
way meant moving everything that decides something out of the composables:
|
||||
`ShellNavigation` (what a tab tap does to the back stack, and that back from a
|
||||
tab goes to Alarms while back from Alarms leaves the app),
|
||||
`LivePillSelector` (which of several running things the pill is about),
|
||||
`ClockFormat`, and `ChallengeGate`. The adaptive part is
|
||||
`NavigationSuiteScaffold` taking its own default — the short bar on a phone,
|
||||
the wide rail on a tablet — rather than a hand-rolled width branch that would
|
||||
`NavigationSuiteScaffold` taking its own default (the short bar on a phone,
|
||||
the wide rail on a tablet) rather than a hand-rolled width branch that would
|
||||
miss the tabletop posture. The pill shows for paused and expired timers too,
|
||||
not only running ones: pausing from the pill must not make the pill disappear
|
||||
under the thumb that pressed it. Its value comes from the snapshot, so a timer
|
||||
that reaches zero flips to "finished" before M6's expiry write lands. The
|
||||
challenge's escape hatch opens on three wrong answers **or** sixty seconds of
|
||||
challenge's escape hatch opens on three wrong answers or sixty seconds of
|
||||
ringing, for every challenge kind, asserted as one parameterised invariant;
|
||||
snooze is never gated; a completed hold *is* the dismissal, and an
|
||||
accessibility click completes the hold, because a challenge TalkBack cannot
|
||||
@@ -200,7 +200,7 @@ plumbing.
|
||||
top-level tab draws no back arrow that would pop nothing.
|
||||
|
||||
Knowingly open: M4 ships no in-app way to *start* a timer or the stopwatch, so
|
||||
the live pill cannot be demonstrated by hand until M6/M7 — it is proven by its
|
||||
the live pill cannot be demonstrated by hand until M6/M7. It is proven by its
|
||||
unit tests and by an instrumentation test that seeds the repository directly,
|
||||
and no debug button was added to paper over it. The nine new instrumentation
|
||||
tests compile in the gate but have never run on hardware, as no device was
|
||||
@@ -213,18 +213,18 @@ List, create, edit, delete, enable/disable. Time picker, repeat-day selector,
|
||||
label, ringtone picker, vibrate, snooze settings, per-alarm overrides. Next-fire
|
||||
line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`.
|
||||
|
||||
✅ 624 JVM tests, 144 of them new, and **still no Robolectric** — kept for a
|
||||
✅ 624 JVM tests, 144 of them new, and still no Robolectric, kept for a
|
||||
whole *form*, which is where a screen usually reaches for one. Everything a
|
||||
tap decides is a pure object (`AlarmListRows`, `NextFireFormat`,
|
||||
`RepeatDaysSummary`, `AlarmDefaults`, `AlarmOverrides`, `RingtonePicker`,
|
||||
`AudioSourcePolicy`, `AlarmRoutes`) or a ViewModel method tested over the
|
||||
**real** `AlarmEngine`; three new `ArchitectureRulesTest` rules make that a
|
||||
real `AlarmEngine`; three new `ArchitectureRulesTest` rules make that a
|
||||
build fact rather than a habit.
|
||||
|
||||
The decisions worth knowing: the next-fire line comes from
|
||||
`AlarmEngine.upcoming`, which grew a `refresh: Flow<Unit>` cadence parameter
|
||||
so the countdown advances without a database write and the screen re-derives
|
||||
none of the engine's time arithmetic; the list is ordered by **time of day**,
|
||||
none of the engine's time arithmetic; the list is ordered by time of day,
|
||||
not by next fire, so rows never swap under a thumb at 07:00; every per-alarm
|
||||
override is an `OptionPicker` whose first row is "App default (On)", because
|
||||
the stored value is ternary and a `Switch` cannot say "I have not chosen";
|
||||
@@ -235,79 +235,79 @@ line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`.
|
||||
read-modify-write, so a one-shot alarm that disabled itself underneath an open
|
||||
editor is not re-enabled by the next unrelated tap.
|
||||
|
||||
The correctness find: disabling or deleting a **ringing** alarm used to clear
|
||||
`ringingSince` without stopping the ring service — whose auto-silence backstop
|
||||
then disarms itself on its own null guard — so the alarm would have sounded
|
||||
The correctness find: disabling or deleting a ringing alarm used to clear
|
||||
`ringingSince` without stopping the ring service (whose auto-silence backstop
|
||||
then disarms itself on its own null guard), so the alarm would have sounded
|
||||
until the 15-minute wake-lock timeout. Both new ViewModels now dismiss through
|
||||
the engine first, conditionally, so a merely snoozed alarm keeps its snooze.
|
||||
|
||||
floret-kit went to **0.6.0** for one change: `CollapsingScaffold` takes a
|
||||
floret-kit went to 0.6.0 for one change: `CollapsingScaffold` takes a
|
||||
`floatingActionButton` slot, forwarded to the underlying `Scaffold`, rather
|
||||
than each app re-deriving the inset maths in a `Box`.
|
||||
|
||||
Knowingly open: no hand reordering of alarms (the time order answers it, and
|
||||
`alarms` has no `sort_order` column), no multi-select or bulk delete, no undo
|
||||
chip after a delete (traded for a confirmation that names the alarm), no
|
||||
repeat presets, and no per-alarm auto-silence — still M10 at the earliest.
|
||||
repeat presets, and no per-alarm auto-silence (still M10 at the earliest).
|
||||
Editing the app-wide defaults every override row offers to follow is M10's
|
||||
settings screen. The seven new instrumentation tests compile in the gate but
|
||||
have **never run on hardware**, as no device was attached.
|
||||
have never run on hardware, as no device was attached.
|
||||
|
||||
### ✅ M6 — Timers
|
||||
Multiple concurrent timers, presets, labels, add/pause/reset/+1min. Foreground
|
||||
service with notification controls, expiry ringing reusing M3's audio path.
|
||||
Elapsed-realtime anchored.
|
||||
|
||||
✅ 843 JVM tests, 219 of them new, and **still no Robolectric** — kept for a
|
||||
✅ 843 JVM tests, 219 of them new, and still no Robolectric, kept for a
|
||||
second screen *and* for a foreground service, which is the other place a
|
||||
project usually gives in. Every decision is a pure object (`TimerReadings`,
|
||||
`TimerExpiry`, `TimerRingPolicy`, `TimerNotificationPolicy`, `TimerPresets`,
|
||||
`TimerDurationEntry`, `TimerListRows`, `TimerRoutes`) or a method on the
|
||||
engine or one of the two ViewModels, tested over the **real**
|
||||
`TimerRepositoryImpl` and the **real** `TimerEngine`. `TimerService` holds no
|
||||
policy and no state beyond one flag — even its two `delay` amounts come from
|
||||
engine or one of the two ViewModels, tested over the real
|
||||
`TimerRepositoryImpl` and the real `TimerEngine`. `TimerService` holds no
|
||||
policy and no state beyond one flag; even its two `delay` amounts come from
|
||||
the engine, so the timings are asserted in a JVM test. Three new
|
||||
`ArchitectureRulesTest` rules pin it, one of which makes a *second*
|
||||
`MediaPlayer` a build failure. The four new instrumentation tests bring the
|
||||
compiled total to 32.
|
||||
|
||||
The decisions worth knowing: the expiry slot is **one** registration on the
|
||||
`ELAPSED_REALTIME_WAKEUP` base — so a `TIME_SET` cannot warp a running timer,
|
||||
The decisions worth knowing: the expiry slot is one registration on the
|
||||
`ELAPSED_REALTIME_WAKEUP` base (so a `TIME_SET` cannot warp a running timer,
|
||||
and a timer never populates `getNextAlarmClock()`, which belongs to the user's
|
||||
next *alarm* — and its fire is an **idempotent sweep carrying no id**, so a
|
||||
next *alarm*), and its fire is an idempotent sweep carrying no id, so a
|
||||
late delivery expires everything due, an early one writes nothing, and the
|
||||
anchors *are* the watermark; the service adds a second, independent trigger
|
||||
for the same sweep, because neither has to be reliable alone; one
|
||||
`systemExempted` foreground service covers the countdown and the ring, alive
|
||||
on the live pill's own "active, not running" predicate, and the countdown
|
||||
holds **no wake lock** (the alarm slot is what wakes the device); an alarm
|
||||
ringing **wins the audio** and the timer's ring is *deferred, not lost*,
|
||||
because the arbitration is a pure function of current state rather than an
|
||||
event — so the reverse case is symmetric and free; two timers expiring
|
||||
holds no wake lock (the alarm slot is what wakes the device); an alarm
|
||||
ringing wins the audio and the timer's ring is deferred, because the
|
||||
arbitration is a pure function of current state rather than an event, so the
|
||||
reverse case is symmetric and free; two timers expiring
|
||||
together share one ring session and one ten-minute window, and each is stopped
|
||||
on its own, because silently resetting both destroys the information "which
|
||||
one finished"; auto-silence stops the noise and leaves the row EXPIRED, which
|
||||
is the load-bearing difference from an alarm; presets are app-wide in
|
||||
DataStore with out-of-range values **dropped, never clamped**; and the
|
||||
DataStore with out-of-range values dropped, never clamped; and the
|
||||
notification's countdown is the platform chronometer, so the service posts
|
||||
once per state change rather than once per second for forty-five minutes.
|
||||
|
||||
Two things the checklist did not name were changed anyway, and are recorded in
|
||||
`ARCHITECTURE.md`: **"+1 min" on an expired timer now resumes it** with
|
||||
`ARCHITECTURE.md`: "+1 min" on an expired timer now resumes it with
|
||||
exactly the extra, in one transaction (M2 left it paused, written before any
|
||||
UI existed and pinned by no test — and resuming is the only way to avoid
|
||||
emitting an intermediate PAUSED frame to the pill and the row); and **the live
|
||||
pill's timer actions now go through `TimerEngine`**, because after M6 there is
|
||||
UI existed and pinned by no test; resuming is the only way to avoid
|
||||
emitting an intermediate PAUSED frame to the pill and the row); and the live
|
||||
pill's timer actions now go through `TimerEngine`, because after M6 there is
|
||||
an AlarmManager registration and a service that have to move with the state.
|
||||
The pill's contract is otherwise unchanged: M6 *extracted* its precedence into
|
||||
`TimerReadings` so the pill, the notification and the ring share one ordering,
|
||||
and `LivePillSelectorTest` passing **unmodified** is the proof.
|
||||
and `LivePillSelectorTest` passing unmodified is the proof.
|
||||
|
||||
No schema change (the database stays at v2; the ring session's one volatile
|
||||
fact is a DataStore record, which is `PLAN.md` §5's own record-versus-rows
|
||||
split and keeps it out of M10's backup), **no new permission** (a timer never
|
||||
takes over the screen — no full-screen intent, no ring activity), and **no
|
||||
floret-kit change**: the kit earned `CollapsingScaffold`'s
|
||||
split and keeps it out of M10's backup), no new permission (a timer never
|
||||
takes over the screen: no full-screen intent, no ring activity), and no
|
||||
floret-kit change: the kit earned `CollapsingScaffold`'s
|
||||
`floatingActionButton` slot at 0.6.0 for exactly this shape of tab, and M6
|
||||
consumes it.
|
||||
|
||||
@@ -318,85 +318,85 @@ Elapsed-realtime anchored.
|
||||
"stop all" when several timers are expired, no undo after a delete (traded for
|
||||
a confirmation, as with alarms), no ramp on a timer's ring, no presets
|
||||
management screen and no editing of `default_timer_duration_millis` or
|
||||
`default_timer_ringtone_uri` — M10's settings screen. `TimerSnapshot.anchorIsStale`
|
||||
`default_timer_ringtone_uri` (M10's settings screen). `TimerSnapshot.anchorIsStale`
|
||||
is still not surfaced in the UI. Of `AlarmClock`'s intent contract only the
|
||||
in-process hook `ShellNavigation.tabForAction` exists, wired to one action;
|
||||
M9 owns the rest. The four new instrumentation tests compile in the gate but
|
||||
have **never run on hardware**, as no device was attached — the same caveat
|
||||
M5's seven carry.
|
||||
have never run on hardware, as no device was attached; M5's seven carry the
|
||||
same caveat.
|
||||
|
||||
Two cases in the new suite are left **red** and are a design conversation
|
||||
rather than a behaviour gap; both are defects in the test, not in the code:
|
||||
Two cases in the new suite are left red. Both are defects in the test, not in
|
||||
the code, so they are a design conversation rather than a behaviour gap:
|
||||
`TimerIntentsTest` §5.17 #3 asserts `containsNoDuplicates` over a list that
|
||||
contains the *same* `requestCodeFor(REQUEST_PAUSE, 1L)` call twice by
|
||||
construction, which no deterministic function can satisfy; and `TimerPrefsTest`
|
||||
§5.10 #11 opens a second DataStore over a live file in the same scope, which
|
||||
DataStore refuses by design — as `SettingsPrefsTest` itself documents.
|
||||
DataStore refuses by design, as `SettingsPrefsTest` itself documents.
|
||||
|
||||
### ✅ M7 — Stopwatch
|
||||
Start/stop/reset, laps with splits and cumulative times, best/worst lap emphasis.
|
||||
Foreground service so it survives backgrounding; laps persist across process
|
||||
death. This is where the big-readout typography gets settled for the whole app.
|
||||
The post-reboot repair is **not** here — M3 took it (`ARCHITECTURE.md` §5);
|
||||
The post-reboot repair is not here: M3 took it (`ARCHITECTURE.md` §5), and
|
||||
M7 owns only how the paused result is presented.
|
||||
|
||||
✅ 968 JVM tests, 125 of them new, and **still no Robolectric** — kept now for
|
||||
✅ 968 JVM tests, 125 of them new, and still no Robolectric, kept now for
|
||||
a third screen and a third service. Everything the tab decides is a pure
|
||||
object (`StopwatchFormat`, `StopwatchReadings`, `LapStats`, `StopwatchLaps`,
|
||||
`StopwatchNotificationPolicy`, `StopwatchRows`) or a method on the engine or
|
||||
the ViewModel, tested over the **real** `StopwatchRepositoryImpl` across the
|
||||
the ViewModel, tested over the real `StopwatchRepositoryImpl` across the
|
||||
real `appendLap` transaction and a real DataStore under a `@TempDir`.
|
||||
`StopwatchService` holds no policy and **no state at all** — not even the
|
||||
timers' one `alreadyAlerted` flag, because nothing here ever alerts — and it
|
||||
`StopwatchService` holds no policy and no state at all (not even the
|
||||
timers' one `alreadyAlerted` flag, because nothing here ever alerts), and it
|
||||
has no `delay`. Two new `ArchitectureRulesTest` rules and two extended; the
|
||||
four new instrumentation tests bring the compiled total to 36.
|
||||
|
||||
The decisions worth knowing: the foreground service is **`specialUse`**, not
|
||||
`systemExempted`, because a stopwatch is not alarm functionality — it
|
||||
The decisions worth knowing: the foreground service is `specialUse`, not
|
||||
`systemExempted`, because a stopwatch is not alarm functionality. It
|
||||
schedules nothing, wakes nothing and rings nothing, and claiming the type
|
||||
would have the app tell the platform something untrue; it holds **no wake
|
||||
lock, ever**, since the elapsed value is derived from a persisted monotonic
|
||||
anchor at read time; the notification is the **platform chronometer counting
|
||||
up**, with its base `now - elapsed` **derived at post time and never stored**,
|
||||
would have the app tell the platform something untrue. It holds no wake
|
||||
lock, ever, since the elapsed value is derived from a persisted monotonic
|
||||
anchor at read time; the notification is the platform chronometer counting
|
||||
up, with its base `now - elapsed` derived at post time and never stored,
|
||||
which is how the run record keeps its "no wall-clock field whatsoever" rule;
|
||||
its channel is `IMPORTANCE_LOW` and silent, because nothing here ever
|
||||
finishes, so there is no alert-once rule and no session window anywhere in the
|
||||
path; the readout is hundredths and it is **two strings**, so the
|
||||
fastest-changing glyphs are not the loudest ones; best and worst need **three**
|
||||
path; the readout is hundredths and it is two strings, so the
|
||||
fastest-changing glyphs are not the loudest ones; best and worst need three
|
||||
laps, ties go to the earliest, and all-equal means neither; laps are capped at
|
||||
**999**, guarded in the engine over one `COUNT(*)` so appending never
|
||||
materialises the rows; and the tab ticks at ~60 Hz **only while running**, so
|
||||
999, guarded in the engine over one `COUNT(*)` so appending never
|
||||
materialises the rows; and the tab ticks at ~60 Hz only while running, so
|
||||
a paused or idle stopwatch costs nothing.
|
||||
|
||||
Three things the checklist did not name were changed anyway, and are recorded
|
||||
in `ARCHITECTURE.md`: **the live pill's stopwatch actions now go through
|
||||
`StopwatchEngine`** (M6 recorded this as knowingly open), with the pill's own
|
||||
contract unchanged and `LivePillSelectorTest` passing **unmodified** as the
|
||||
proof; **`LivePillSelector` was re-pointed at a new pure `StopwatchReadings`**,
|
||||
in `ARCHITECTURE.md`: the live pill's stopwatch actions now go through
|
||||
`StopwatchEngine` (M6 recorded this as knowingly open), with the pill's own
|
||||
contract unchanged and `LivePillSelectorTest` passing unmodified as the
|
||||
proof; `LivePillSelector` was re-pointed at a new pure `StopwatchReadings`,
|
||||
the same extraction M6 made for timers, so the pill, the shade and the tab
|
||||
cannot answer "what does this read right now" three different ways; and
|
||||
**`ui/theme/Type.kt` stopped being `Typography()`** — tabular figures on the
|
||||
`ui/theme/Type.kt` stopped being `Typography()`: tabular figures on the
|
||||
display, headline and title roles, app-wide, with no call-site change
|
||||
anywhere, plus `ClockulaReadoutDefaults` naming the three roles a readout uses.
|
||||
|
||||
The correctness find: a run that met a reboot mid-lap had `accumulated = 0`
|
||||
while its laps still recorded forty seconds, so the honest reading is
|
||||
**floored at the last lap's total** as well as at zero — a readout sitting
|
||||
floored at the last lap's total as well as at zero. A readout sitting
|
||||
below a lap the same run printed is not one anyone can believe, and the laps
|
||||
are real measurements that are never deleted to tidy the arithmetic. Recorded
|
||||
in `ARCHITECTURE.md` §5. The review found the other half of it: the floor has
|
||||
to be **banked, not only displayed** — resuming writes
|
||||
to be banked as well as displayed. Resuming writes
|
||||
`accumulated = max(accumulated, lastLapCumulative)`, and pausing and lapping
|
||||
bank the same floored reading — or the record ticks from below its own last
|
||||
lap, the shade's free-running chronometer counts while the tab sits frozen,
|
||||
and the next lap records a cumulative below the previous one. The same review
|
||||
pass moved the engine's verbs onto **the reading's** mode rather than the
|
||||
stored row's, so a stale record's Resume button resumes instead of hitting a
|
||||
silent no-op. `pauseAfterReboot` itself is still untouched.
|
||||
bank the same floored reading; otherwise the record ticks from below its own
|
||||
last lap, the shade's free-running chronometer counts while the tab sits
|
||||
frozen, and the next lap records a cumulative below the previous one. The
|
||||
same review pass moved the engine's verbs onto the reading's mode rather than
|
||||
the stored row's, so a stale record's Resume button resumes instead of hitting
|
||||
a silent no-op. `pauseAfterReboot` itself is still untouched.
|
||||
|
||||
No schema change (the database stays at v2; `LapDao` gains one `COUNT(*)` and
|
||||
nothing else), **one** new permission — `FOREGROUND_SERVICE_SPECIAL_USE`, the
|
||||
one that names what the service actually does — and **no floret-kit change**:
|
||||
nothing else), one new permission (`FOREGROUND_SERVICE_SPECIAL_USE`, the
|
||||
one that names what the service actually does), and no floret-kit change:
|
||||
the kit stays at 0.6.0, untouched.
|
||||
|
||||
Knowingly open: no lap export, sharing, deletion or editing, and no "previous
|
||||
@@ -409,86 +409,86 @@ M7 owns only how the paused result is presented.
|
||||
is still not surfaced, the same answer M6 gave for timers. Of `AlarmClock`'s
|
||||
intent contract only the in-process hook `ShellNavigation.tabForAction` grew a
|
||||
second action; M9 owns the rest. The four new instrumentation tests compile in
|
||||
the gate but have **never run on hardware**, as no device was attached — the
|
||||
same caveat M5's seven and M6's four carry.
|
||||
the gate but have never run on hardware, as no device was attached; M5's
|
||||
seven and M6's four carry the same caveat.
|
||||
|
||||
### ✅ M8 — World clock
|
||||
IANA zone list with search, ICU-localised city and zone display names, offset and
|
||||
day-difference relative to home, reorder, home-zone handling. The analog face
|
||||
built from `MaterialShapes` / `androidx.graphics.shapes` — the app's one
|
||||
built from `MaterialShapes` / `androidx.graphics.shapes` is the app's one
|
||||
deliberate showpiece.
|
||||
|
||||
✅ 1 139 JVM tests, 171 of them new, and **still no Robolectric** — kept now
|
||||
for a fourth screen, and this was the milestone most tempted to break it,
|
||||
✅ 1 139 JVM tests, 171 of them new, and still no Robolectric, kept now
|
||||
for a fourth screen. This was the milestone most tempted to break it,
|
||||
because ICU and `Canvas` both look like they need a device. They do not:
|
||||
everything ICU answers is behind `ZoneNames` (faked, with per-method call
|
||||
counters and a `failEverything` flag), everything the face draws is
|
||||
`DrawScope` calls over `AnalogFace`'s angles and `DayNight`'s fraction — two
|
||||
pure objects asserted at every minute of the day without a pixel — and
|
||||
`DrawScope` calls over `AnalogFace`'s angles and `DayNight`'s fraction (two
|
||||
pure objects asserted at every minute of the day without a pixel), and
|
||||
everything the list and the picker decide is `WorldClockRows`,
|
||||
`ZonePickerRows`, `ZoneSearch`, `ZoneComparisons`, `ZoneCatalog` and
|
||||
`WorldClocks`. Every write is a `WorldClockViewModel` method over the **real**
|
||||
`WorldClocks`. Every write is a `WorldClockViewModel` method over the real
|
||||
`WorldClockRepositoryImpl` across `FakeWorldClockDao` (so `addIfAbsent` and
|
||||
`reorder`'s real transactions run) and a real DataStore under a `@TempDir`.
|
||||
Three new `ArchitectureRulesTest` rules; the four new instrumentation tests
|
||||
bring the compiled total to 40.
|
||||
|
||||
The decisions worth knowing: **there is no engine and must not be one** — a
|
||||
The decisions worth knowing: there is no engine and must not be one, because a
|
||||
world clock owns no AlarmManager slot, no service and no ring session, so a
|
||||
fourth engine would be three empty seams and a `Mutex` guarding nothing; the
|
||||
tab reads the **wall clock**, and re-reads both the instant and the device
|
||||
zone on **every tick**, which is what makes `TIME_SET` and `TIMEZONE_CHANGED`
|
||||
free (no new receiver branch); the zone **ids** stay pure (through a new
|
||||
`ZoneProvider.available()`) while only the **names** cross the ICU seam, which
|
||||
tab reads the wall clock, and re-reads both the instant and the device
|
||||
zone on every tick, which is what makes `TIME_SET` and `TIMEZONE_CHANGED`
|
||||
free (no new receiver branch); the zone ids stay pure (through a new
|
||||
`ZoneProvider.available()`) while only the names cross the ICU seam, which
|
||||
is one file, `runCatching`-guarded end to end and pinned there by a build
|
||||
rule; the catalog keeps only region-based ids that are **their own canonical
|
||||
id** — so the tzdata's own canonicalisation drops Asia/Calcutta in favour of
|
||||
Asia/Kolkata and no table of ours can rot — unless the canonical id is not on
|
||||
rule; the catalog keeps only region-based ids that are their own canonical
|
||||
id (so the tzdata's own canonicalisation drops Asia/Calcutta in favour of
|
||||
Asia/Kolkata and no table of ours can rot), unless the canonical id is not on
|
||||
the device at all, since ICU and tzdata are separately updatable APEX modules
|
||||
that can disagree, plus `UTC` by name; search is
|
||||
**word-prefix over folded text**, so "erl" deliberately does not find Berlin;
|
||||
the offset and day are computed **at the instant** from both zones' rules, the
|
||||
word-prefix over folded text, so "erl" deliberately does not find Berlin;
|
||||
the offset and day are computed at the instant from both zones' rules, the
|
||||
same discipline `AlarmOccurrences` uses, so Berlin↔Sydney is ten hours in
|
||||
January and eight in July; labels are **data, not strings**, so Kathmandu is
|
||||
January and eight in July; labels are data, not strings, so Kathmandu is
|
||||
`Ahead(4, 45)` rather than a signed 285 for a translator to interpret; the
|
||||
cadence is **200 ms** with the state truncated to the second (face) and the
|
||||
cadence is 200 ms with the state truncated to the second (face) and the
|
||||
minute (rows) and `distinctUntilChanged` over it; the list is capped at
|
||||
**24**, with the cap spelled out on the screen once it is reached, because
|
||||
24, with the cap spelled out on the screen once it is reached, because
|
||||
M3's `ExtendedFloatingActionButton` has no `enabled` to dim; and
|
||||
reduce-motion snaps the morph but **never stops the second hand**,
|
||||
reduce-motion snaps the morph but never stops the second hand,
|
||||
because a clock whose second hand stops is a broken clock.
|
||||
|
||||
Three things outside the checklist were changed anyway, and are recorded in
|
||||
`ARCHITECTURE.md`: **`ZoneProvider` gained `available()`**, because
|
||||
`ARCHITECTURE.md`: `ZoneProvider` gained `available()`, because
|
||||
`ZoneId.getAvailableZoneIds()` at a call site is the same ambient read
|
||||
`PLAN.md` §5 bans for `now()`; **`ui/common/AlarmTimeFormatter.kt` gained
|
||||
`rememberLocalTimeFormatter`** with the existing function delegating to it, so
|
||||
`PLAN.md` §5 bans for `now()`; `ui/common/AlarmTimeFormatter.kt` gained
|
||||
`rememberLocalTimeFormatter` with the existing function delegating to it, so
|
||||
M5's "one 12/24-hour formatter" survives a fourth caller instead of growing a
|
||||
second answer; and **`ShellDefaults.EmptyTabScreen` was deleted**, its own
|
||||
KDoc having said it existed only until each of M5–M8 replaced its own tab —
|
||||
second answer; and `ShellDefaults.EmptyTabScreen` was deleted, its own
|
||||
KDoc having said it existed only until each of M5–M8 replaced its own tab;
|
||||
the world clock was its last caller.
|
||||
|
||||
No schema change at all (the database stays at v2; `WorldClockEntity`, its
|
||||
DAO, its mapper and its repository are not edited), **no new permission and no
|
||||
manifest entry**, **one** new dependency coordinate —
|
||||
`androidx.graphics:graphics-shapes:1.0.1`, pinned to the version that already
|
||||
resolved transitively, because M8 imports that package directly — and **no
|
||||
floret-kit change**: the kit stays at 0.6.0, untouched, with
|
||||
DAO, its mapper and its repository are not edited), no new permission and no
|
||||
manifest entry, one new dependency coordinate
|
||||
(`androidx.graphics:graphics-shapes:1.0.1`, pinned to the version that already
|
||||
resolved transitively, because M8 imports that package directly), and no
|
||||
floret-kit change: the kit stays at 0.6.0, untouched, with
|
||||
`ReorderableColumn`, `GroupedSurface`, `FullScreenPicker`, `InlineTextField`
|
||||
and `CollapsingScaffold`'s FAB slot already covering the screen.
|
||||
|
||||
Knowingly open: no rename UI for `world_clocks.label` — the column is honoured
|
||||
on read and M10's backup import will carry labels; no automatic "home" row
|
||||
Knowingly open: no rename UI for `world_clocks.label` (the column is honoured
|
||||
on read and M10's backup import will carry labels); no automatic "home" row
|
||||
while travelling (a separate feature with its own preference, and the hero
|
||||
face already follows the device); no per-city detail screen, map,
|
||||
sunrise/sunset or day-length bar — `DayNight` is a stated convention for a
|
||||
shape morph, not an ephemeris; no live times or long zone names in the picker;
|
||||
sunrise/sunset or day-length bar (`DayNight` is a stated convention for a
|
||||
shape morph, not an ephemeris); no live times or long zone names in the picker;
|
||||
no seeded data, so a fresh install really has an empty list; no reordering by
|
||||
anything but `sort_order`; and no `MaterialShapes` on the ring screen or timer
|
||||
progress, which `PLAN.md` §8 also sanctions — the build rule makes widening
|
||||
progress, which `PLAN.md` §8 also sanctions; the build rule makes widening
|
||||
that allowlist a deliberate edit. The four new instrumentation tests compile
|
||||
in the gate but have **never run on hardware**, as no device was attached —
|
||||
the same caveat M5's seven, M6's four and M7's four carry.
|
||||
in the gate but have never run on hardware, as no device was attached;
|
||||
M5's seven, M6's four and M7's four carry the same caveat.
|
||||
|
||||
### ✅ M9 — System interop
|
||||
The full `android.provider.AlarmClock` contract (`PLAN.md` §6): `SET_ALARM`,
|
||||
@@ -496,103 +496,103 @@ The full `android.provider.AlarmClock` contract (`PLAN.md` §6): `SET_ALARM`,
|
||||
next-alarm publishing. Hostile-input validation at the intent boundary.
|
||||
- **Tests:** extra validation, including malformed and out-of-range input.
|
||||
|
||||
✅ 1 420 JVM tests, 281 of them new, and **still no Robolectric** — kept now
|
||||
✅ 1 420 JVM tests, 281 of them new, and still no Robolectric, kept now
|
||||
for a *boundary*, which is the last place a project usually gives in, because
|
||||
"it is an `Intent`" sounds like it needs a device. It does not: the door is
|
||||
split so that everything which decides is pure (`AlarmClockRequests`,
|
||||
`AlarmMatching`, `IntentExtras`, `InteropText`, `InteropDeepLinks`) and the
|
||||
Android half is two policy-free files — a `Bundle` reader and an `Intent`
|
||||
builder. `AlarmClockHandler` orchestrates with **no `android.*` import**, as
|
||||
all three engines do, and is tested over both **real** engines through a new
|
||||
`InteropHarness` — deliberately *not* a composition of the existing harnesses,
|
||||
since each builds a DataStore over the same file name and DataStore refuses a
|
||||
second instance over a live one. Four new `ArchitectureRulesTest` rules and a
|
||||
new `ManifestRulesTest`, because the manifest is where this milestone could be
|
||||
silently wrong and none of it fails a compiler. The five new instrumentation
|
||||
tests bring the compiled total to 45.
|
||||
Android half is two policy-free files: a `Bundle` reader and an `Intent`
|
||||
builder. `AlarmClockHandler` orchestrates with no `android.*` import, as
|
||||
all three engines do, and is tested over both real engines through a new
|
||||
`InteropHarness`. It is deliberately not a composition of the existing
|
||||
harnesses, since each builds a DataStore over the same file name and DataStore
|
||||
refuses a second instance over a live one. Four new `ArchitectureRulesTest`
|
||||
rules and a new `ManifestRulesTest`, because the manifest is where this
|
||||
milestone could be silently wrong and none of it fails a compiler. The five
|
||||
new instrumentation tests bring the compiled total to 45.
|
||||
|
||||
`ACTION_DISMISS_TIMER` — the seventh action, named neither in this checklist
|
||||
nor in `PLAN.md` §6's table — was built anyway: "the full contract" is the
|
||||
`ACTION_DISMISS_TIMER`, the seventh action, named neither in this checklist
|
||||
nor in `PLAN.md` §6's table, was built anyway: "the full contract" is the
|
||||
milestone's own first sentence, and shipping six of seven and calling it full
|
||||
would be the kind of quiet gap this loop exists to avoid.
|
||||
|
||||
The decisions worth knowing: **one range rule settles every extra** — a value
|
||||
that decides when something will ring in the *future* is **dropped** out of
|
||||
range, a value that modifies what the user is doing *now* is **clamped**, so
|
||||
The decisions worth knowing: one range rule settles every extra. A value
|
||||
that decides when something will ring in the *future* is dropped out of
|
||||
range, and a value that modifies what the user is doing *now* is clamped, so
|
||||
hour 25 never becomes an enabled 23:00 alarm while a 1 000-minute snooze
|
||||
becomes the 60 minutes `ClockPrefs` would have allowed; **a `Bundle` is
|
||||
modelled honestly** as `Map<String, Any?>` with four *total* accessors,
|
||||
becomes the 60 minutes `ClockPrefs` would have allowed; a `Bundle` is
|
||||
modelled honestly as `Map<String, Any?>` with four *total* accessors,
|
||||
because hostile input is an `Int` where a `String` was documented far more
|
||||
often than it is an out-of-range number, and the platform's typed getters
|
||||
cannot tell "absent" from "wrong type"; **`SKIP_UI` never means skip
|
||||
validation** — an incomplete `SET_ALARM` creates the row from the extras that
|
||||
cannot tell "absent" from "wrong type"; `SKIP_UI` never means skip
|
||||
validation, so an incomplete `SET_ALARM` creates the row from the extras that
|
||||
*were* valid, at the next whole hour, and lands the user in its editor, since
|
||||
M5 locked that the editor edits a persisted row and nothing is ever written
|
||||
and hidden; **`ALARM_SEARCH_MODE_TIME` matches exactly, never "nearest"**, and
|
||||
and hidden; `ALARM_SEARCH_MODE_TIME` matches exactly, never "nearest", and
|
||||
an ambiguous 0..12 hour with no `IS_PM` carries *both* readings as candidates
|
||||
so the question only reaches the user when the data cannot settle it;
|
||||
**`ALARM_SEARCH_MODE_ALL` never asks** — taken literally the contract's "show
|
||||
the results" would make the mode useless — while every other search with two
|
||||
`ALARM_SEARCH_MODE_ALL` never asks (taken literally, the contract's "show
|
||||
the results" would make the mode useless), while every other search with two
|
||||
or more matches opens the milestone's one new surface, an M3 basic dialog with
|
||||
a `ListItem` list where tapping a row *is* the answer.
|
||||
|
||||
**A correctness find, fixed here:** the `AlarmClockInfo` **`showIntent`** — the
|
||||
A correctness find, fixed here: the `AlarmClockInfo` `showIntent` (the
|
||||
thing the system opens when the user taps the status-bar alarm icon or the
|
||||
lockscreen's next-alarm line — pointed at the *ring screen* with
|
||||
lockscreen's next-alarm line) pointed at the *ring screen* with
|
||||
`CLEAR_TASK`, so tapping "my next alarm" opened a ring screen for an alarm
|
||||
that was not ringing, which resolved to `Finished` and closed itself. It now
|
||||
opens `MainActivity` on the Alarms tab, and a build rule fails the build if
|
||||
`AndroidAlarmScheduler` ever names `AlarmRingActivity` again. That intent is
|
||||
literally what "next-alarm publishing" means to a user.
|
||||
what "next-alarm publishing" means to a user.
|
||||
|
||||
**The schema went to v3**: `delete_after_use` on `alarms` and on `timers`, two
|
||||
The schema went to v3: `delete_after_use` on `alarms` and on `timers`, two
|
||||
`ALTER TABLE`s in `MIGRATION_2_3`, `3.json` exported and committed, `1.json`
|
||||
and `2.json` untouched. It is not optional polish — the contract says in two
|
||||
places that a `SKIP_UI` alarm or timer should be removed once dismissed, and
|
||||
without it a daily "wake me at 6:30" turns the Alarms tab into a graveyard.
|
||||
and `2.json` untouched. The contract says in two places that a `SKIP_UI`
|
||||
alarm or timer should be removed once dismissed, and without the column a
|
||||
daily "wake me at 6:30" turns the Alarms tab into a graveyard.
|
||||
The flag is set only on the `SKIP_UI` create path and honoured inside the two
|
||||
engines, under their locks.
|
||||
|
||||
Four things outside the checklist were changed anyway, and are recorded in
|
||||
`ARCHITECTURE.md`: **`AlarmEngine` gained `dismissUpcoming(id)` and a
|
||||
`minutesOverride` on `snooze`**, because both move the ring slot, the
|
||||
`ARCHITECTURE.md`: `AlarmEngine` gained `dismissUpcoming(id)` and a
|
||||
`minutesOverride` on `snooze`, because both move the ring slot, the
|
||||
auto-silence backstop and the next-alarm registration, and a handler calling
|
||||
three repositories in a row would race a fire broadcast; **`TimerEngine` gained
|
||||
`dismissAllExpired()` and `dismissExpired(id)`** — the first is the "stop all"
|
||||
three repositories in a row would race a fire broadcast; `TimerEngine` gained
|
||||
`dismissAllExpired()` and `dismissExpired(id)` (the first is the "stop all"
|
||||
the UI still deliberately does not offer, because that rule was about a thumb
|
||||
on a ring and a programmatic request has stated exactly what it is discarding;
|
||||
**`domain/text/TextFolding` was extracted** and `ZoneSearch` delegates to it,
|
||||
with `ZoneSearchTest` passing unmodified as the proof; and **the shell's
|
||||
`openTab` became `openRequest: ShellRequest?`**, since three of the contract's
|
||||
on a ring and a programmatic request has stated exactly what it is discarding);
|
||||
`domain/text/TextFolding` was extracted and `ZoneSearch` delegates to it,
|
||||
with `ZoneSearchTest` passing unmodified as the proof; and the shell's
|
||||
`openTab` became `openRequest: ShellRequest?`, since three of the contract's
|
||||
outcomes are not "select a tab".
|
||||
|
||||
**No new `<uses-permission>`**: the one new permission string is
|
||||
`android:permission` *on the door*, a requirement on the caller. **No new
|
||||
dependency coordinate** and **no floret-kit change** — the kit stays at 0.6.0,
|
||||
No new `<uses-permission>`: the one new permission string is
|
||||
`android:permission` *on the door*, a requirement on the caller. No new
|
||||
dependency coordinate and no floret-kit change; the kit stays at 0.6.0,
|
||||
untouched, because the one new surface is material3's own `AlertDialog`.
|
||||
|
||||
Knowingly open: no voice-interaction follow-on flows
|
||||
(`VoiceInteractor.CompleteVoiceRequest`/`PickOptionRequest`), so
|
||||
`android.intent.category.VOICE` is deliberately not declared and Clockula
|
||||
accepts a `clockula://` deeplink while publishing none; no "most closely
|
||||
matched" time search; no toast on a `SKIP_UI` write — the status-bar icon is
|
||||
the platform's own receipt; no `delete_after_use` surface in either editor, so
|
||||
matched" time search; no toast on a `SKIP_UI` write (the status-bar icon is
|
||||
the platform's own receipt); no `delete_after_use` surface in either editor, so
|
||||
an assistant-set alarm the user edits stays transient, as it does in Google
|
||||
Clock; no `EXTRA_MESSAGE` carried into the timer setup panel when no length
|
||||
was given; and `AlarmScheduler.systemNextAlarm()` is still caller-less — it
|
||||
was given; and `AlarmScheduler.systemNextAlarm()` is still caller-less, since it
|
||||
belongs to M10's self-check screen. The five new instrumentation tests and the
|
||||
two new migration cases compile in the gate but have **never run on
|
||||
hardware**, as no device was attached — the same caveat M5's seven, M6's four,
|
||||
M7's four and M8's four carry.
|
||||
two new migration cases compile in the gate but have never run on
|
||||
hardware, as no device was attached; M5's seven, M6's four, M7's four and
|
||||
M8's four carry the same caveat.
|
||||
|
||||
### ✅ M10 — Settings, backup, the self-check, and widgets
|
||||
- Settings composed from kit components: theme, dynamic colour, language,
|
||||
alarm/timer/clock defaults, about + crash reporting.
|
||||
- JSON backup export/import via SAF (`PLAN.md` §7), schema documented in
|
||||
`docs/BACKUP.md`.
|
||||
- The **"why might my alarm not ring?"** self-check screen (`PLAN.md` §4) —
|
||||
- The "why might my alarm not ring?" self-check screen (`PLAN.md` §4):
|
||||
real device state, deep links to the exact settings pages.
|
||||
- Three Glance home-screen widgets — next alarm, timer, clock (`PLAN.md` §9).
|
||||
- Three Glance home-screen widgets: next alarm, timer, clock (`PLAN.md` §9).
|
||||
- **Tests:** backup round-trip and unknown-field tolerance; self-check rule
|
||||
logic per permission/state combination; widget model selection/formatting.
|
||||
|
||||
@@ -608,13 +608,13 @@ reliability soak across a few real nights. Then 1.0.0.
|
||||
|
||||
Deferred deliberately, in rough priority order:
|
||||
|
||||
- **Dock / screensaver mode** — a `DreamService` full-screen clock. Cheapest big
|
||||
- **Dock / screensaver mode**: a `DreamService` full-screen clock. Cheapest big
|
||||
win, and the surface people look at most.
|
||||
- **Quick Settings tile** — timer control and next alarm from the shade.
|
||||
- **Bedtime / sleep schedule** — wind-down and wake, with DND handoff. A feature
|
||||
- **Quick Settings tile**: timer control and next alarm from the shade.
|
||||
- **Bedtime / sleep schedule**: wind-down and wake, with DND handoff. A feature
|
||||
in its own right, realistically v2.
|
||||
- **`core-notification` in floret-kit** — revisit once Clockula's real
|
||||
- **`core-notification` in floret-kit**: revisit once Clockula's real
|
||||
notification surface is known (`PLAN.md` §3).
|
||||
- **Migrate Calendula and Agendula onto `core-prefs` + `core-di`** — tracked in
|
||||
- **Migrate Calendula and Agendula onto `core-prefs` + `core-di`**: tracked in
|
||||
the kit's roadmap, not here.
|
||||
- **Wear OS companion** — unscoped.
|
||||
- **Wear OS companion**: unscoped.
|
||||
@@ -4,7 +4,7 @@ It is the third app in the Floret family, after Calendula (calendar) and Agendul
|
||||
|
||||
<b>Alarms that actually ring</b>
|
||||
|
||||
An alarm app is only worth having if you can trust it. Clockula schedules alarms the way Android intends — as real alarm clocks, exempt from battery saving, visible to the system lock screen and status bar. When something on your device would stop an alarm from ringing, Clockula tells you which setting it is and takes you there, instead of failing silently at 7am.
|
||||
Clockula schedules alarms the way Android intends: as real alarm clocks, exempt from battery saving and visible on the lock screen and status bar. When something on your device would stop an alarm from ringing, Clockula tells you which setting it is and takes you there, instead of failing silently at 7am.
|
||||
|
||||
<b>Everything you expect</b>
|
||||
|
||||
@@ -17,6 +17,6 @@ A running timer or stopwatch stays reachable from every tab, so you never lose t
|
||||
|
||||
<b>Open, and yours</b>
|
||||
|
||||
Clockula speaks Android's standard alarm intents, so your assistant and your automation apps can set alarms and timers in it like any other clock app. Your alarms, timers and clocks export to a documented JSON file whenever you want them, because data you cannot take with you is not really yours.
|
||||
Clockula speaks Android's standard alarm intents, so your assistant and your automation apps can set alarms and timers in it like any other clock app. Your alarms, timers and clocks export to a documented JSON file whenever you want them.
|
||||
|
||||
No account. No tracking. No ads. Free software under the MIT licence.
|
||||
No account, no tracking, no ads. Free software under the MIT licence.
|
||||
Reference in new issue
Block a user