382 lines
22 KiB
Markdown
382 lines
22 KiB
Markdown
# Clockula — roadmap
|
|
|
|
The **status** view, and the work queue. Design rationale for every decision here
|
|
lives in [`PLAN.md`](PLAN.md); the shape of the code as it stands will live in
|
|
[`ARCHITECTURE.md`](ARCHITECTURE.md) from M2 onward.
|
|
|
|
Status legend: ✅ done · 🚧 in progress · ⬜ not started
|
|
|
|
---
|
|
|
|
## Current state (one line)
|
|
|
|
✅ **M6 done.** The Timers tab is real: several timers at once, a keypad and
|
|
saved presets that set one in a couple of taps, pause/reset/"+1 min" from the
|
|
row, the live pill *and* a notification whose countdown the system draws, a
|
|
foreground service alive exactly while something is active, and expiry that
|
|
rings through M3's audio path — waiting its turn when an alarm is already
|
|
ringing rather than talking over it. Every countdown is anchored on the device's
|
|
uptime, so moving the system clock cannot move a timer in either direction. Two
|
|
tabs are still empty states: **M7** (stopwatch — start/stop/reset, laps with
|
|
splits, and the app-wide big-readout typography settled at last) is next.
|
|
|
|
---
|
|
|
|
## How the loop works through this
|
|
|
|
One milestone per iteration, auto-continuing. Each iteration:
|
|
|
|
1. Pick the **first milestone not marked ✅**.
|
|
2. Implement it **completely** — every checklist item under it.
|
|
3. Build, and run the tests the milestone names. A milestone is not done until
|
|
they pass.
|
|
4. **Commit** (floret-kit submodule first where it changed, then Clockula's
|
|
pointer bump — separate commits, granular, lowercase area prefix).
|
|
5. Mark the milestone ✅ here, update **Current state**, and commit that too.
|
|
6. Continue to the next milestone.
|
|
|
|
Rules that hold across every iteration:
|
|
|
|
- **Never let a Room entity or `@Query` string leak above the data layer**
|
|
(`PLAN.md` §0).
|
|
- **Never** add a foojay toolchain resolver, anywhere, not even in a comment.
|
|
- Commit each finished unit of work; never accumulate a WIP pile.
|
|
- If a milestone turns out to be wrong, say so and amend `PLAN.md` — do not
|
|
silently build something else.
|
|
- `ARCHITECTURE.md` and `CHANGELOG.md` are updated as part of the milestone that
|
|
changes them, not in a later cleanup pass.
|
|
|
|
---
|
|
|
|
## v1 milestones
|
|
|
|
### ✅ M0 — Skeleton & pipeline
|
|
The project exists, builds, is themed, and ships itself.
|
|
- Gradle project: `settings.gradle.kts`, version catalog copied from Agendula
|
|
(drop `:provider`, add Room + KSP), AGP 9.x conventions, `vcsInfo` off on
|
|
release, no foojay resolver.
|
|
- floret-kit as a git submodule + `includeBuild`, with the gitignored
|
|
`floret-kit/local.properties` arrangement documented.
|
|
- Hilt, Compose, `MainActivity`, `FloretExpressiveTheme` reseeded to `#6B7A5C`
|
|
(`ui/theme/Color.kt` + `Theme.kt`), hand-tuned light/dark schemes.
|
|
- `core-crash` wired; `core-locale` + `res/xml/locales_config.xml`.
|
|
- Codeberg repo, canonical from the first commit. Full pipeline per `PLAN.md` §10:
|
|
`ci.yaml`, `translations.yaml`, `release.yaml` (Agendula spine + Calendula `play`
|
|
job), `renovate.yml`, issue/PR templates, F-Droid metadata, fastlane tree.
|
|
- `README.md`, `LICENSE` (MIT), `CHANGELOG.md`, `.editorconfig`, `.gitattributes`,
|
|
`.gitignore`, `docs/README.md` index.
|
|
- **Done when:** it builds, installs, shows a themed placeholder, and CI is green.
|
|
|
|
✅ `assembleDebug`, `lintDebug` and `testDebugUnitTest` all pass;
|
|
`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
|
|
describe.
|
|
|
|
### ✅ M1 — floret-kit: `core-prefs` + `core-di`
|
|
Pay the extraction the kit's roadmap has been waiting for a third app to trigger.
|
|
- In the kit: `core-prefs` (`ThemeMode`, `dynamicColor`, typed DataStore wrapper,
|
|
`toEnum()`) and `core-di` (`@IoDispatcher` + provider), each with tests, module
|
|
docs, `CHANGELOG` and `ROADMAP`/`ARCHITECTURE` updates.
|
|
- Clockula consumes both. Migrating Calendula and Agendula is explicitly **out of
|
|
scope** — note it in the kit's roadmap as a follow-up.
|
|
- **Done when:** the kit's tests pass, Clockula builds against the new modules,
|
|
and both repos are committed (submodule first, then the pointer bump).
|
|
|
|
✅ 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.
|
|
`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
|
|
reads the stored preference, with the DataStore built on a corruption handler
|
|
so an unreadable prefs file cannot brick startup.
|
|
|
|
### ✅ M2 — Data layer
|
|
The whole storage stack, headless. No UI.
|
|
- Room: `alarms`, `timers`, `world_clocks`, `stopwatch_laps` (`PLAN.md` §5),
|
|
schema exported and committed, DAOs.
|
|
- Domain models (plain Kotlin) + mappers. Repositories exposing Flows.
|
|
- DataStore prefs (theme, dynamic colour, defaults, stopwatch running state).
|
|
- DI modules.
|
|
- **Tests:** mappers, repository behaviour, the elapsed-realtime vs wall-clock
|
|
distinction under a simulated clock change.
|
|
- Write the first `ARCHITECTURE.md`.
|
|
- **Done when:** the schema is exported and committed, no Room type or query
|
|
string appears above `data/`, and the build, lint and unit tests pass.
|
|
|
|
✅ Four tables (`alarms`, `timers`, `world_clocks`, `stopwatch_laps`), schema
|
|
v1 exported 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.
|
|
|
|
### ✅ M3 — Alarm engine
|
|
The hard part (`PLAN.md` §4), still headless apart from the ring screen's
|
|
plumbing.
|
|
- Next-fire resolution: local time-of-day + repeat mask → next instant in the
|
|
device zone; `skipNextOccurrence`; snooze.
|
|
- `AlarmScheduler` over `setAlarmClock`, single-next-alarm registration,
|
|
`USE_EXACT_ALARM` with a `SCHEDULE_EXACT_ALARM` fallback path.
|
|
- Receivers: fire, `BOOT_COMPLETED`, `TIME_SET`, `TIMEZONE_CHANGED`,
|
|
`MY_PACKAGE_REPLACED`.
|
|
- Ringing foreground service: audio focus, `USAGE_ALARM`, volume ramp, vibration,
|
|
ringtone URIs; survives process death and reboot mid-ring.
|
|
- Full-screen-intent notification + `canUseFullScreenIntent()` handling, degrading
|
|
to a high-priority heads-up notification that still rings — never to silence.
|
|
- **Tests:** DST spring-forward and fall-back, every repeat configuration, skip,
|
|
snooze-vs-next-occurrence. These are the tests the app lives or dies on.
|
|
|
|
✅ 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 —
|
|
`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
|
|
instant, which is the whole DST story: a 02:30 alarm rings at 03:30 on a
|
|
spring-forward night instead of vanishing, and once rather than twice on a
|
|
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
|
|
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.
|
|
|
|
### ✅ M4 — App shell
|
|
- Navigation host, four tabs, M3 navigation bar, adaptive rail on wide layouts.
|
|
- The **live pill** (`PLAN.md` §9) — shared running-state surface, pause + stop
|
|
from any tab.
|
|
- The ring screen UI: over-lockscreen, turn-screen-on, snooze/dismiss, optional
|
|
dismiss challenge that cannot strand the user.
|
|
- Motion from the kit's `identity` module; predictive back.
|
|
|
|
✅ 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),
|
|
`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
|
|
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
|
|
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
|
|
answer is exactly the stranding the requirement forbids. floret-kit went to
|
|
0.5.0 for one change: `CollapsingScaffold`'s `onBack` is nullable, so a
|
|
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
|
|
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
|
|
attached. `POST_NOTIFICATIONS` is asked for once on first launch; explaining a
|
|
denial, and the exact-alarm and full-screen-intent grant deep links, moved
|
|
wholly to M10's self-check, which is the screen with room to say why.
|
|
|
|
### ✅ M5 — Alarms screen
|
|
List, create, edit, delete, enable/disable. Time picker, repeat-day selector,
|
|
label, ringtone picker, vibrate, snooze settings, per-alarm overrides. Next-fire
|
|
line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`.
|
|
|
|
✅ 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
|
|
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**,
|
|
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";
|
|
silence is an explicit sentinel (`clockula://silent`) that resolves to an
|
|
empty audio-source list and therefore forces vibration, so a "silent" alarm
|
|
is still an alarm; an unreadable sound is reported, never rewritten, because
|
|
an unmounted card comes back; and every editor write is a transactional
|
|
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
|
|
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
|
|
`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.
|
|
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.
|
|
|
|
### ✅ 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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
|
|
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
|
|
`floatingActionButton` slot at 0.6.0 for exactly this shape of tab, and M6
|
|
consumes it.
|
|
|
|
Knowingly open: no hand reordering of timers (`sort_order` and
|
|
`TimerRepository.reorder` stay caller-less, as they do for alarms), no
|
|
per-timer vibrate, volume-ramp or dismiss-challenge overrides (`timers` has
|
|
one nullable settings column and four more would be a schema change), no
|
|
"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`
|
|
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.
|
|
|
|
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:
|
|
`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.
|
|
|
|
### ⬜ 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);
|
|
M7 owns only how the paused result is presented.
|
|
|
|
### ⬜ M8 — World clock
|
|
IANA zone list with search, ICU-localised city and zone display names, offset and
|
|
day-difference relative to home, reorder, home-zone handling. The analog face
|
|
built from `MaterialShapes` / `androidx.graphics.shapes` — the app's one
|
|
deliberate showpiece.
|
|
|
|
### ⬜ M9 — System interop
|
|
The full `android.provider.AlarmClock` contract (`PLAN.md` §6): `SET_ALARM`,
|
|
`SET_TIMER`, `SHOW_ALARMS`, `SHOW_TIMERS`, `DISMISS_ALARM`, `SNOOZE_ALARM`, plus
|
|
next-alarm publishing. Hostile-input validation at the intent boundary.
|
|
- **Tests:** extra validation, including malformed and out-of-range input.
|
|
|
|
### ⬜ M10 — Settings, backup, and the self-check
|
|
- Settings composed from kit components: theme, dynamic colour, language,
|
|
alarm/timer/clock defaults, about + crash reporting.
|
|
- JSON backup export/import via SAF (`PLAN.md` §7), schema documented in the repo.
|
|
- The **"why might my alarm not ring?"** self-check screen (`PLAN.md` §4) —
|
|
real device state, deep links to the exact settings pages.
|
|
- **Tests:** backup round-trip and unknown-field tolerance.
|
|
|
|
### ⬜ M11 — Release readiness
|
|
Translations wired to Weblate, fastlane metadata and screenshots, F-Droid
|
|
reproducibility guard verified against the submodule, instrumentation smoke
|
|
tests, accessibility pass (TalkBack on the ring screen especially), on-device
|
|
reliability soak across a few real nights. Then 1.0.0.
|
|
|
|
---
|
|
|
|
## Post-v1
|
|
|
|
Deferred deliberately, in rough priority order:
|
|
|
|
- **Dock / screensaver mode** — a `DreamService` full-screen clock. Cheapest big
|
|
win, and the surface people look at most.
|
|
- **Glance home-screen widget** — next alarm and running timer.
|
|
- **Quick Settings tile** — timer control and next alarm from the shade.
|
|
- **Bedtime / sleep schedule** — wind-down and wake, with DND handoff. A feature
|
|
in its own right, realistically v2.
|
|
- **`core-notification` in floret-kit** — revisit once Clockula's real
|
|
notification surface is known (`PLAN.md` §3).
|
|
- **Migrate Calendula and Agendula onto `core-prefs` + `core-di`** — tracked in
|
|
the kit's roadmap, not here.
|
|
- **Wear OS companion** — unscoped.
|