docs: mark M10 done
ARCHITECTURE.md's package table gets the new domain/backup, domain/selfcheck, domain/widget, data/backup, backup/, widget/, ui/settings/ and ui/widget/ entries, and §9/§10 are brought up to date with what M10 actually built (the exported-component count, the permission list unchanged, which 'not built yet' rows are now done). CHANGELOG and ROADMAP's current-state line follow.
This commit is contained in:
@@ -181,6 +181,23 @@ Tag sections feed the release notes — see [`docs/RELEASING.md`](docs/RELEASING
|
||||
- An alarm or timer set by voice without showing you a screen removes itself
|
||||
once it has been dismissed, so talking to your assistant every morning no
|
||||
longer leaves a list of dead 06:30 alarms behind.
|
||||
- A settings screen, reachable from a gear icon on every tab: theme, dynamic
|
||||
colour and language, the alarm/timer/clock defaults every new alarm or timer
|
||||
inherits, help, and the app version.
|
||||
- Back up your alarms, timers and world clocks to a JSON file in a folder you
|
||||
choose, and restore them on this device or another one. Restoring replaces
|
||||
everything currently on the device — the app says so before you confirm.
|
||||
Nothing about a ringing alarm or a running timer's live state travels with
|
||||
the backup, only the configuration.
|
||||
- A "why might my alarm not ring?" screen that checks the actual state of your
|
||||
device — exact-alarm and notification permissions, full-screen alerts,
|
||||
battery optimisation, Do Not Disturb, your alarm volume, and whether the
|
||||
system has actually scheduled Clockula's next alarm — and links straight to
|
||||
whichever settings page would fix each one.
|
||||
- Three home-screen widgets: your next alarm, a running or paused timer (with
|
||||
pause, resume and reset right on the widget), and a clock that grows to show
|
||||
your world clocks at larger sizes. Each one updates the moment something
|
||||
changes — no polling, no stale numbers.
|
||||
|
||||
### Changed
|
||||
- Tapping the alarm icon in the status bar, or the next-alarm line on the lock
|
||||
|
||||
+25
-14
@@ -96,6 +96,9 @@ kit module — the kit's roadmap keeps schedulers app-local.
|
||||
| `domain/text/` | `TextFolding` (M9) — NFD-fold, strip combining marks, lowercase through `Locale.ROOT`, `containsFolded`. One copy, shared by the zone search and the label search |
|
||||
| `domain/live/` | `LivePillSelector` and its state — which running thing the live pill is about, as a pure function |
|
||||
| `domain/format/` | `ClockFormat` — `M:SS` / `H:MM:SS`, countdowns rounded up, elapsed truncated — `NextFire` (`NextFireLabel` + `NextFireFormat`), the alarm row's "in 9h 12m" as data, and `StopwatchFormat` — the hundredths readout, split into a major field and two digits so the fraction can be drawn smaller |
|
||||
| `domain/backup/` | `BackupContent` (M10) — the plain Kotlin model between `data/backup`'s JSON codec and the repository/engine; decoupled from the wire DTOs the same way the data layer is decoupled from Room |
|
||||
| `domain/selfcheck/` | `SelfCheck` (M10) — the "why might my alarm not ring?" rule table: pure functions over a `DeviceState` snapshot, each producing a `CheckResult` with a status and an optional deep-link `SelfCheckFix` |
|
||||
| `domain/widget/` | the three widget presenters (M10) — `NextAlarmWidgetModel`/`TimerWidgetModel`/`ClockWidgetModel`: selection and formatting only, so a widget's content is unit-tested without Glance |
|
||||
| `data/db/` | `ClockulaDatabase` — `@Database` v3, `exportSchema = true` — and `Migrations` |
|
||||
| `data/alarms/` | the alarm and ring-state entities, DAOs, mappers and repositories |
|
||||
| `data/timers/` | `TimerEntity`, `TimerDao`, `TimerMapper`, `TimerRepository(+Impl)`, `TimerRingStateStore` (the ring session's one DataStore record) |
|
||||
@@ -105,7 +108,8 @@ kit module — the kit's roadmap keeps schedulers app-local.
|
||||
| `data/prefs/` | `SettingsPrefs`, `ClockPrefs`, `StopwatchPrefs`, `TimerPrefs`, `UiPrefs` |
|
||||
| `data/ringtones/` | the two ringtone seams — `RingtoneCatalog` (the device's alarm sounds, titles, playability, a SAF grant) and `RingtonePreviewer` — plus their `System*` implementations |
|
||||
| `data/time/` | `SystemWallClock`, `SystemElapsedRealtimeClock`, `SystemZoneProvider`, `AndroidBootIdProvider`, `RealTicker` |
|
||||
| `data/di/` | `DataModule`, `DatabaseModule`, `RepositoryModule`, `TimeModule`, `RingtoneModule`, `ZoneModule` (M8 — one `@Binds`, `ZoneNames → IcuZoneNames`) |
|
||||
| `data/di/` | `DataModule`, `DatabaseModule`, `RepositoryModule`, `TimeModule`, `RingtoneModule`, `ZoneModule` (M8 — one `@Binds`, `ZoneNames → IcuZoneNames`), `BackupModule` (M10) |
|
||||
| `data/backup/` | the backup's data-layer half (M10): `BackupDto`/`BackupCodec` (the pure JSON wire format, `ignoreUnknownKeys`, strict on known fields), `BackupDao` (one `@Transaction` replace-all over alarms/timers/world clocks — no schema or version change), `BackupRepository(+Impl)` — mapping through the existing `AlarmMapper`/`TimerMapper`/`WorldClockMapper`, never a second copy |
|
||||
| `alarm/` | `AlarmEngine` and the four seams it talks to — `AlarmScheduler`, `AlarmCapabilities`, `RingCoordinator`, `AlarmNotifier` — plus `AlarmIntents` |
|
||||
| `alarm/android/` | the seams' Android implementations: AlarmManager, the capability reads, the service handle, the snoozed notification |
|
||||
| `alarm/receiver/` | `AlarmFireReceiver`, `AlarmActionReceiver`, `SystemEventReceiver` |
|
||||
@@ -124,6 +128,8 @@ kit module — the kit's roadmap keeps schedulers app-local.
|
||||
| `stopwatch/di/` | `StopwatchModule` — `@Binds` for the one seam |
|
||||
| `interop/` | the `AlarmClock` boundary's Android half (M9): `AlarmClockActivity` (the one exported, permission-guarded, window-less door), `IntentExtrasReader` (`Bundle` → `IntentExtras`), `AlarmClockHandler` (the orchestrator — **no `android.*` import, ever**), `InteropIntents` (the internal transport to `MainActivity`) and `InteropLaunch` |
|
||||
| `system/` | `RebootRepair` — the boot-id gate |
|
||||
| `backup/` | `BackupEngine` (M10) — the orchestrator: a ringing alarm dismissed and running timers deleted before a replace, `AlarmEngine.reschedule()`/`TimerEngine.resync()` after it — plus `BackupFiles`, the SAF seam, with its Android implementation in `backup/android/` |
|
||||
| `widget/` | the three `GlanceAppWidget`/`GlanceAppWidgetReceiver` pairs (M10) and `WidgetSync` — the event-driven collector, started from `ClockulaApp.onCreate`, that redraws all three on any change to the state they mirror. `widget/di/` holds the `@EntryPoint` Glance needs, since a `GlanceAppWidgetReceiver` is never `@AndroidEntryPoint`-constructed |
|
||||
| `ui/theme/`, `ui/crash/` | the M0/M1 theme and the crash-report surface |
|
||||
| `ui/shell/` | the navigation policy (`ShellNavigation`), the adaptive shell, the live pill and its source/ViewModel, the notification-permission ask. `EmptyTabScreen` is **gone**: its KDoc said it existed only until each of M5–M8 replaced its own tab, and the world clock was its last caller |
|
||||
| `ui/common/` | `rememberLocalTimeFormatter` — the app's **one** 12/24-hour formatter, over a `java.time.LocalTime`, with `rememberAlarmTimeFormatter` delegating to it (M8 D17) — and, since M6, the ringtone picker (`RingtonePickerState`, `RingtonePickerScreen`), which both editors need |
|
||||
@@ -132,6 +138,8 @@ kit module — the kit's roadmap keeps schedulers app-local.
|
||||
| `ui/stopwatch/` | the real Stopwatch tab (M7): the pure row builder (`StopwatchRows`) and its source, the ViewModel, the readout panel with its controls, the lap table and its column header, `StopwatchDefaults` |
|
||||
| `ui/worldclock/` | the real World clock tab (M8): the pure row builders (`WorldClockRows`, `ZonePickerRows`) and their state, `WorldClockSource`, the ViewModel, the tab, the analog face, the row and the zone picker, `WorldClockDefaults` |
|
||||
| `ui/ring/` | `AlarmRingActivity`, its ViewModel, `RingScreen` and the dismiss-challenge controls |
|
||||
| `ui/settings/` | the settings hub (M10): `SettingsScreen`/`SettingsViewModel` (appearance, alarm/timer/clock defaults, reliability, data, help), `SelfCheckScreen`/`SelfCheckViewModel`, `BackupScreen`/`BackupViewModel` — all reachable from a gear icon on every tab, with no owning tab of their own |
|
||||
| `ui/widget/` | the three widgets' `@Composable` Glance content (M10) — the only Compose this milestone adds outside a tab; the `GlanceAppWidget`/`GlanceAppWidgetReceiver` classes themselves live in `widget/`, since they are manifest components, not UI |
|
||||
|
||||
---
|
||||
|
||||
@@ -1025,11 +1033,17 @@ nothing flashes on the way through.
|
||||
|
||||
Components: `MainActivity`, the exported and permission-guarded
|
||||
`interop.AlarmClockActivity`, the non-exported `CrashReportActivity` and
|
||||
`AlarmRingActivity`, two foreground services (`AlarmRingService` and M6's
|
||||
`TimerService`), five receivers — `AlarmFireReceiver`, `AlarmActionReceiver`,
|
||||
`SystemEventReceiver`, `TimerExpiryReceiver`, `TimerActionReceiver` — and
|
||||
AppCompat's locale metadata holder service. Exactly **two** of them are
|
||||
exported, and `ManifestRulesTest` fails the build on a third.
|
||||
`AlarmRingActivity`, three foreground services (`AlarmRingService`, M6's
|
||||
`TimerService`, M7's `StopwatchService`), five non-widget receivers —
|
||||
`AlarmFireReceiver`, `AlarmActionReceiver`, `SystemEventReceiver`,
|
||||
`TimerExpiryReceiver`, `TimerActionReceiver` — AppCompat's locale metadata
|
||||
holder service, and, since M10, three `GlanceAppWidgetReceiver`s
|
||||
(`NextAlarmWidgetReceiver`, `TimerWidgetReceiver`, `ClockWidgetReceiver`).
|
||||
`ManifestRulesTest` pins the exported set exactly: the two above, plus the
|
||||
three widget receivers — each with **only** the platform-required
|
||||
`APPWIDGET_UPDATE` filter and no permission, since `APPWIDGET_UPDATE` is a
|
||||
platform-delivered broadcast no other app can trigger by naming the action.
|
||||
M10 adds no `<uses-permission>`.
|
||||
|
||||
---
|
||||
|
||||
@@ -1037,22 +1051,19 @@ exported, and `ManifestRulesTest` fails the build on a third.
|
||||
|
||||
| Not here | Milestone |
|
||||
|---|---|
|
||||
| The exact-alarm and full-screen-intent grant deep links, and any explanation of a denial. M4 asks for `POST_NOTIFICATIONS` once on first launch; the rest waits for the self-check screen, because a bare jump into system settings with no reason given is hostile | M10 |
|
||||
| Reordering alarms by hand. The list is ordered by time of day (§13) and `alarms` has no `sort_order` column, so this would be a schema change for a preference the time order already answers | not planned for v1 |
|
||||
| A per-alarm auto-silence duration. Every other per-alarm setting became an override picker in M5; this one is still global and still `AlarmRing.AUTO_SILENCE_AFTER` | M10 at the earliest |
|
||||
| A per-alarm auto-silence duration. Every other per-alarm setting became an override picker in M5; this one is still global and still `AlarmRing.AUTO_SILENCE_AFTER` | not planned for v1 |
|
||||
| Voice-interaction follow-on flows: `Activity.isVoiceInteraction`, `VoiceInteractor.CompleteVoiceRequest` (publishing a deeplink to the alarm just created) and `PickOptionRequest` (disambiguating by voice). `android.intent.category.VOICE` is therefore not declared — advertising it while answering with a touch dialog would be a promise the app does not keep. Clockula *accepts* a `clockula://` deeplink and publishes none | not planned for v1 |
|
||||
| A "most closely matched" time search for `DISMISS_ALARM`. `ALARM_SEARCH_MODE_TIME` matches **exactly**: dismissing an alarm the caller did not name is a missed alarm, and there is no distance at which that becomes a good trade | not planned for v1 |
|
||||
| A toast or any other confirmation on a `SKIP_UI` write. `SKIP_UI` means "bypass any intermediate UI", a voice assistant speaks its own confirmation, and the status-bar alarm icon `setAlarmClock` lights is the platform's own receipt (§17) | not planned for v1 |
|
||||
| Surfacing `delete_after_use` in either editor. It is a property the contract sets, not a setting; an alarm the user edits keeps it, and that consequence is documented (§17) rather than papered over with a checkbox | not planned for v1 |
|
||||
| Carrying `EXTRA_MESSAGE` into the timer setup panel when no length was given. There is no row to carry it on and the panel has no label field | not planned for v1 |
|
||||
| Verifying a ringtone URI at the intent boundary. M5 locked "an unreadable sound is reported, never rewritten"; an unmounted card comes back, and the boundary opens no `ContentResolver` | not planned for v1 |
|
||||
| Exporting `delete_after_use` in a backup. There is no backup yet; M10's JSON schema must include the column | M10 |
|
||||
| The self-check screen ("why might my alarm not ring?"), settings screen, JSON backup / SAF export | M10 |
|
||||
| A user-configurable auto-silence duration, an unlimited-snooze option, per-alarm auto-silence | M10 at the earliest |
|
||||
| A user-configurable auto-silence duration, an unlimited-snooze option, per-alarm auto-silence | not planned for v1 |
|
||||
| Reordering timers by hand. `sort_order` and `TimerRepository.reorder` exist and stay caller-less: the list is in storage order, M5 gave the same answer for alarms, and a drag surface would also mean abandoning the `LazyColumn` | not planned for v1 |
|
||||
| Per-timer vibrate, volume-ramp or dismiss-challenge overrides. `timers` has exactly one nullable settings column, its ringtone; four more would be a schema change for settings the roadmap does not ask for | M10 at the earliest |
|
||||
| Per-timer vibrate, volume-ramp or dismiss-challenge overrides. `timers` has exactly one nullable settings column, its ringtone; four more would be a schema change for settings the roadmap does not ask for | not planned for v1 |
|
||||
| A "stop all" for several expired timers, and undo after a timer delete. Each expired timer is acknowledged on its own, and the delete confirmation asks before rather than after | not planned for v1 |
|
||||
| A presets management screen, and editing `default_timer_duration_millis` or `default_timer_ringtone_uri`. M6 reads both and offers the two-gesture preset surface on the setup panel | M10 |
|
||||
| A presets management screen (add/remove the quick-start durations themselves). M10's settings screen edits `default_timer_duration_millis` and `default_timer_ringtone_uri`, but the preset list is still only editable from the two-gesture surface on the setup panel | not planned for v1 |
|
||||
| A timer that ramps its volume, a dismiss challenge or a snooze for a timer | not planned for v1 |
|
||||
| Surfacing `TimerSnapshot.anchorIsStale` in the UI. The boot repair makes it vanishingly rare and the value is already honest; a row does not say "estimated" in v1 | not planned for v1 |
|
||||
| Lap export, sharing, deletion or editing, and any "previous run" history. A run's laps belong to the run that produced them; keeping old runs is a different feature with its own table | not planned for v1 |
|
||||
@@ -1062,7 +1073,7 @@ exported, and `ManifestRulesTest` fails the build on a third.
|
||||
| A countdown-to-start, split-lap alarms, or any sound or vibration from the stopwatch. It is silent, and a build rule says so (§8) | not planned for v1 |
|
||||
| An analog stopwatch face or a `MaterialShapes` showpiece on the Stopwatch tab. `PLAN.md` §8 sanctions the showpiece for the analog world-clock face, the ring screen and timer progress; a stopwatch has no progress to be a fraction of | not planned for v1 |
|
||||
| Surfacing `StopwatchReading.anchorIsStale` in the UI, for the same reason the timers' is not: the repair makes it momentary, and the value displayed is already honest | not planned for v1 |
|
||||
| A rename UI for `world_clocks.label`. The column is honoured on **read** — a row shows the label when it has one — but M8 adds no way to write it; M10's backup import will carry labels | M10 |
|
||||
| A rename UI for `world_clocks.label`. The column is honoured on **read** — a row shows the label when it has one — and M10's backup now carries it on export/import, but there is still no way to *write* a label in-app | not planned for v1 |
|
||||
| An automatic "home" row while travelling. Google Clock's automatic home clock is a separate feature with its own preference and its own edge cases; M8's answer to "where am I" is the hero face, which already follows the device when no home zone is stored | not planned for v1 |
|
||||
| A per-city detail screen, a map, sunrise/sunset times or a day-length bar. `DayNight` is a stated convention for a shape morph, not an ephemeris (§16) | not planned for v1 |
|
||||
| Live times or long zone names in the zone picker. 450 rows of ICU long names is real work for a list the user scrolls past, and a ticking picker is a ticking picker | not planned for v1 |
|
||||
|
||||
+14
-15
@@ -10,20 +10,19 @@ Status legend: ✅ done · 🚧 in progress · ⬜ not started
|
||||
|
||||
## Current state (one line)
|
||||
|
||||
✅ **M9 done.** Every app on the device — an assistant, an automation app, a
|
||||
watch companion, a shell script — can now drive Clockula through
|
||||
`android.provider.AlarmClock`, and no extra it sends can make Clockula write
|
||||
something the user did not ask for. All **seven** of the contract's actions are
|
||||
answered through **one** exported, permission-guarded, window-less door, behind
|
||||
which everything that decides is pure Kotlin: an hour of 25 is dropped rather
|
||||
than clamped to 23, a snooze of 1 000 minutes is clamped rather than refused, a
|
||||
`file://` ringtone inherits the default instead of blocking the alarm, and an
|
||||
intent hostile in every extra at once still leaves exactly one alarm and the
|
||||
user looking at it. A voice-set alarm is **transient** — the schema went to v3
|
||||
for it — so "wake me at 6:30" every morning no longer leaves a graveyard of dead
|
||||
06:30 rows. And the status-bar alarm icon finally opens the alarms list instead
|
||||
of a ring screen for an alarm that is not ringing. **M10** (settings, JSON
|
||||
backup and the self-check screen) is next.
|
||||
✅ **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
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -586,7 +585,7 @@ next-alarm publishing. Hostile-input validation at the intent boundary.
|
||||
hardware**, as no device was attached — the same caveat M5's seven, M6's four,
|
||||
M7's four and M8's four carry.
|
||||
|
||||
### ⬜ M10 — Settings, backup, the self-check, and widgets
|
||||
### ✅ 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
|
||||
|
||||
Reference in New Issue
Block a user