Commit Graph
112 Commits
Author SHA1 Message Date
makiolaj face2edb8d fix(ringtones): let a sound preview be stopped
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Failing after 10s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Skipped
The picker's play button only ever started a preview; nothing but taking
a row, playing another one or leaving the picker stopped it. The
previewer now publishes what it is sounding, cleared on stop and when the
tone ends, and the playing row's button becomes a stop button.
2026-10-02 22:33:20 +02:00
makiolaj fe64521707 fix(widgets): keep GlanceAppWidget classes distinct under R8
R8 merged the near-identical widget classes (timer, quick timer,
stopwatch, digital clock) into the analog clock's class. Glance finds a
widget's placed instances by its GlanceAppWidget's class name, so every
updateAll hit every widget: starting a quick timer redrew the timer
widgets' content into the analog and digital clocks until their config
screen re-rendered them. Debug builds were unaffected.
2026-10-02 22:33:20 +02:00
makiolaj a1337e7d9a fix(widgets): keep WorkManager's InputMerger constructor under R8
R8 under AGP 9 pruned the no-arg constructor of OverwritingInputMerger,
which WorkManager instantiates reflectively before running any worker.
Every Glance session worker then failed with "Could not create Input
Merger", no RemoteViews reached the host, and every widget showed
"Can't load widget" in minified builds. Debug builds were unaffected.
2026-10-02 22:15:05 +02:00
makiolaj a8bc652b22 feat(settings): call the app "Clock" and add an app icon picker
The launcher and system label is now a plain, translatable "Clock"; the
brand name Clockula stays on the About card and in crash reports via an
untranslated app_brand_name string.

Settings → Appearance → App icon chooses between the bloom mark and a new
plain clock icon (the same dial and hands without the badge). The launcher
entry moves off MainActivity onto two <activity-alias> components, exactly
one enabled at a time, as Calendula does for its launcher name. The
component-enabled state is the single source of truth, and switching
enables the target before disabling the other so the launcher never sees
zero entries.
2026-10-02 20:55:09 +02:00
makiolaj f37c5f2f84 feat(ringtones): let a music app play the alarm
"Choose from a music app…" in the sound picker browses any app that shares
its library through Android's MediaBrowserService and stores the choice as a
clockula://media URI in the existing ringtone field — no schema change, no
new permission, no internet. The music app streams with its own.

At ring time the app is asked to play through its media session. Every wait
is bounded and any failure falls back to the ordinary alarm sound: the app
won't connect or start, stalls or buffers too long, or the media stream
might not be heard (headphones or a classic Bluetooth device connected, Do
Not Disturb muting media). The ramp drives the media stream and the user's
media volume is restored when the last media ring ends.

RingAudioPlayer.start no longer suspends: vibration starts up front, the
timer collector is not held up, and a generation token keeps late work from
reviving a stopped ring.
2026-10-02 20:44:58 +02:00
makiolaj 767b9ed72d chore(fastlane): add store listing icon 2026-10-02 20:18:24 +02:00
makiolaj acd99964ae fix(widgets): keep android:tint in picker previews, suppress UseAppTint
The previews are inflated as RemoteViews by the launcher, where AppCompat's
app:tint never applies, so lint's suggested fix would drop the tint.
2026-10-02 19:18:14 +02:00
makiolaj bad996d946 feat(widgets): stopwatch style picker and widget-picker previews
The stopwatch widget gets a per-instance style, chosen in a new
StopwatchConfigActivity on the APPWIDGET_CONFIGURE contract. Every widget
receiver now carries its own label, with a separate description, and the
widget picker shows sample-value preview layouts instead of a bare name.
2026-10-02 19:16:36 +02:00
makiolaj 7f2b047182 feat(settings): category hub with sub-screens, matching Agendula and Calendula
The flat settings list becomes a hub of icon-chip category rows that slide
into their own screens: Appearance, Alarms, Timers and Clock. The About card
carries the app logo and a support row; source, licence and privacy links
move to an About group at the foot, above a plain version line.

Report a problem now surfaces a pending crash report first, replacing the
separate "Send last crash report" row.
2026-10-02 19:16:32 +02:00
makiolaj 520bf379b4 docs: add privacy policy 2026-10-02 19:11:18 +02:00
makiolaj 0f5cf4f1eb fix(widgets): config saves, live world clocks, quick-timer guards
- Config screens share WidgetConfigActivity: a picker that dismisses right
  after selecting no longer finishes with RESULT_CANCELED (which removed a
  newly placed widget on API 29/30), and the save runs to completion.
- Digital config shows only once stored settings are read, so an early OK
  or a late load can't write defaults over them.
- Digital widget collects world clocks inside the composition, so edits
  reach a live session.
- Quick timer: a second tap while its timer is active starts nothing;
  durations are capped at 59:59 (1 h preset dropped) to fit the readout.
- Analog date hand is redrawn at local midnight and on time/zone changes
  (AnalogDateRefresh); updatePeriodMillis back to 0.
- WidgetSync routes each flow only to the widgets that read it.
- Stopwatch widget uses the ElapsedRealtimeClock seam and has a two-row
  minimum height.
- Remove dead quick-timer helpers; share showWorldClockIntent.
2026-10-02 18:18:04 +02:00
makiolaj 82fff19d3d feat(widgets): M3 Expressive widget lineup — analog, digital, stopwatch, quick timer
Six widgets, each with minimal settings, styled with M3 Expressive shapes and
Material You colours, and sized proportionally to the placed widget
(SizeMode.Exact + LocalSize):

- Analog clock (new): nine faces on MaterialShapes dials (Round, Numeral,
  Day, Cookie, Sunny, Scallop, Clover, Square, Petal) with per-face hands,
  a seconds dot and Google-Clock-style orbiting date on some faces, and a
  day/date pill on others. Style picker previews inflate the real views.
- Digital clock: rebuilt as RemoteViews faces (Classic, Thin, Bold, Pill,
  Stacked) shared by the widget and its config previews; world clocks are
  opt-in and only on the card faces.
- Next alarm and Timers: shape badges, icon buttons, proportional readouts;
  a lone timer on a tall widget gets a stacked layout.
- Stopwatch (new): large readout with play/pause pill and lap/reset.
- Quick timer (new): a Scallop that is a timer of its own — tap to start a
  transient timer of its configured duration, tap again to reset/stop.

Colours resolve in the launcher (widget_* palette resources, GlanceTheme's
dynamic colours) so widgets follow day/night and the wallpaper. Data widgets
collect their flows inside the composition so a live Glance session never
shows stale state, chronometer bases use elapsedRealtime, and config
activities close cleanly on an invalid widget id.
2026-10-02 18:01:11 +02:00
makiolaj 5d03c654bf 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.
2026-10-01 22:34:07 +02:00
makiolaj fb81bfbf2e feat(widgets): next alarm, timer and clock home-screen widgets
Three Glance widgets, mirroring state the app already computes rather
than owning any of their own: the next alarm (AlarmEngine.upcoming()),
the live pill's subject timer (same selection logic, no ticker of its
own — a running countdown is the platform chronometer, exactly like
the ring notification), and the local time growing to world clocks at
larger sizes. WidgetSync collects the same read-only flows the shell
already reads and redraws on change, event-driven (updatePeriodMillis
= 0) — it touches no engine, only re-renders, and covers every write
path including SystemEventReceiver's since the process is already
alive whenever those run.

Tapping a widget reuses the same internal navigation actions the
AlarmClock contract's door already produces; the timer buttons send
the same unexported broadcasts its notification does. A widget is
just another caller of machinery that already exists.

ManifestRulesTest is updated deliberately: three GlanceAppWidgetReceivers
join the exported set, each pinned to exactly the platform-required
APPWIDGET_UPDATE filter and no permission — no new permission is
requested anywhere in this change.
2026-10-01 22:32:07 +02:00
makiolaj a11396b058 feat(selfcheck): why might my alarm not ring?
A pure rule table (domain/selfcheck/SelfCheck.kt) over real device
state: exact-alarm and full-screen-intent permission, notification and
alarm-channel state, battery-optimisation exemption, background
restriction, DND (read-only — Clockula never requests
ACCESS_NOTIFICATION_POLICY or touches DND itself), alarm stream
volume, a known aggressive-OEM allowlist, and the system's own next
alarm compared against what Clockula itself computed. Each row maps to
a deep link into the exact settings page responsible.

FSI denial and a missing battery exemption warn rather than fail —
both degrade (to heads-up, and to a doze-exempt setAlarmClock
registration) rather than silence the alarm. The settings hub's
'Why might my alarm not ring?' row now shows the live problem count.
2026-10-01 22:15:42 +02:00
makiolaj 616bf7f67f feat(backup): JSON export/import through SAF, whole-state replace
clockula-backup-v1.json, schema documented in docs/BACKUP.md per
PLAN.md §7: alarms, timers and world clocks as configuration only —
ring state and a timer's running anchors never leave the device, since
neither means anything on another one. Unknown fields are ignored at
any level; a malformed known field rejects the whole file rather than
importing it half-way.

Import is whole-state replacement, not a merge, and the backup screen
says so before the user confirms. Before replacing: a ringing alarm is
dismissed and every running timer is deleted, so the import can never
strand a live ring session; after it, AlarmEngine and TimerEngine are
told to re-resolve so the imported state is live immediately.

BackupDao adds delete-all/insert-many queries under one transaction —
no schema or version change, queries only.
2026-10-01 22:06:44 +02:00
makiolaj 7253445c4e feat(settings): a settings hub — appearance, alarm/timer/clock defaults, data, help
Reachable from a gear icon on every tab, composed from kit components
(GroupedRow/OptionPicker/AboutCard/LanguagePickerRow) following
Calendula's hub shape: About card first, then headed groups. Every
control writes immediately — no Save button, same as the alarm editor.

Appearance: theme, dynamic colour (API 31+), language. Alarm and timer
defaults reuse the editor's own ringtone and override pickers
(OverridePickers gets an includeAppDefault flag so the settings rows
don't offer an 'inherit' option that has nothing above it to inherit
from) and the world clock's home-zone picker. Reliability and Data
rows are placeholders for the self-check and backup screens landing
next. Help surfaces the issue tracker, a pending crash report (the
same dialog MainActivity already shows on launch), and the app
version.

Settings has no owning tab — ShellNavigation.selectedDestinationOf
deliberately leaves it unmapped, so the shell falls back to Alarms
while it's open, same as any other unrecognised route.
2026-10-01 17:36:06 +02:00
makiolaj 0664791ac5 test(manifest): fix ManifestRulesTest permission-list drift
POST_PROMOTED_NOTIFICATIONS was added to AndroidManifest.xml for the
timer's Live Update chip but never added to this test's expected
permission list — pre-existing drift, unrelated to M10, caught while
touching this file for settings work.
2026-10-01 17:35:55 +02:00
makiolaj 3266807ffe build: add kotlinx.serialization and Glance for M10
A pure-Kotlin JSON codec (so the backup round-trip test runs on the
JVM) and Jetpack Glance (the three M10 widgets). Both resolve from
google()/mavenCentral() only, no verification-metadata file exists to
update, and the repro guard checks vcsInfo/dependenciesInfo/foojay —
none of which this touches.
2026-10-01 17:23:24 +02:00
makiolaj c8cc156691 docs: bring home-screen widgets into M10, out of post-v1
Widgets share M10's machinery end to end: the next-alarm and timer
state a widget mirrors is exactly what the settings/backup/self-check
work already touches, and a widget tap is just another caller of the
MainActivity door the AlarmClock contract already opens. Keeping them
parked in post-v1 would mean building the same read paths twice.

QS tile, screensaver and bedtime stay deferred — none of them shares
M10's surface the way widgets do.

Also tightens PLAN.md §4's DND wording (interruption-filter read, no
ACCESS_NOTIFICATION_POLICY) and §7's backup semantics (whole-state
replace, ring state excluded, Room column vocabulary) ahead of M10's
implementation.
2026-10-01 17:22:07 +02:00
makiolajandClaude Sonnet 5 d47220947b feat(timers): redo the add-timer flow as a full-screen calculator keypad
The Timers tab gets a full rewrite of how a timer is created, after Google
Clock as the interaction reference (PLAN.md §11):

- A dedicated TimerSetupScreen replaces the bottom-sheet/inline setup panel:
  a per-unit "00h 00m 00s" readout, a "00" key, and a big centred play
  button that never shifts position when Clear fades in beside it.
- The keypad grows into the thumb zone on its own screen (92dp keys, pushed
  toward the bottom), and both it and the readout read bold.
- Presets are dropped from the flow; TimerDurationEntry gains the matching
  domain support (hoursSegment/minutesSegment/secondsSegment,
  plusDoubleZero()).
- An empty timer list shows the same readout/keypad/Start inline, in place
  of a "tap + to add" card — the keypad is still the empty state, just the
  redesigned one.
- Timer cards are disconnected like the Alarms tab's own list, each with an
  instant, unconfirmed delete via a small corner button (re-creating a
  timer is faster than reading a confirmation dialog would be).
- Hero readouts go bold app-wide to match: stopwatch, alarm rows, world
  clock, the ring screen, and both editors' length/time displays
  (ClockulaReadoutDefaults.Hero and matching call sites).

Alongside: Android 16 Live Update support for the running-timer notification
(POST_PROMOTED_NOTIFICATIONS, ProgressStyle), the live pill restyled to a
plain tonal circle, the alarm dismiss-chooser dialog moved onto Floret's
OptionCard, and small fixes to addTime's zero/negative-result handling,
repeat-day selection and alarm routes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 17:06:54 +02:00
makiolaj 3ff74dc1d5 docs: mark M9 done
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 8s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Failing after 6m50s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Skipped
2026-09-23 11:02:24 +02:00
makiolaj 9ab3927efe docs: the system boundary, and what it refuses
`ARCHITECTURE.md` gains §17: the door, the split between the Android-free
validator and the reader that guards the unparcel, the range rule (clamp what
is happening now, drop what would define a future ring), the candidate table
for "which alarm did you mean", and the three deliberate narrowings — including
why a link we cannot read dismisses nothing rather than everything.

§4 records schema v3 and its migration; §13 records the reading of `PLAN.md` §6
this milestone builds, since a malformed intent lands the user in an editor
over a row created from the valid extras, there being no "new alarm" sentinel.
2026-09-23 11:02:20 +02:00
makiolaj 46f625b6b2 test(arch): the platform's intents stay at the door, and the show intent stays pointed
Three new rules. Nothing under `domain/` may name `android.content.Intent`, so
the validator stays testable without a device. The `AlarmClock` action strings
may be named only under `domain/interop/` and `interop/`. And
`AndroidAlarmScheduler` may not name `AlarmRingActivity` — the defect this
milestone fixed would otherwise come back the next time someone reaches for a
"show the alarm" intent.
2026-09-23 11:02:20 +02:00
makiolaj 46a049f988 feat(alarms): ask which alarm, when the request names more than one
"Dismiss my alarm" with two alarms set is a question, not an instruction. When
the search matches more than one the app asks, in a dialog listing the
candidates; when it matches exactly one it acts without asking.

The Timers tab's setup effect no longer re-keys on a list that changes with
every readout tick.
2026-09-23 11:02:20 +02:00
makiolaj 02f3794ee8 feat(shell): navigation the system can ask for, read once per launch
An incoming request is more than a tab now — it can open an editor, compose a
timer or ask which alarm to dismiss. So it is read once, when the activity is
created rather than on every configuration change: re-deriving it on a rotation
would drag the user back into an editor they had just left.

The extras are unparcelled behind a guard here too. `MainActivity` is exported,
and before this it only ever read the action, which never unparcels.
2026-09-23 11:02:06 +02:00
makiolaj e55f3d19d4 feat(interop): one exported door for the whole AlarmClock contract
`AlarmClockActivity` is the only exported surface that takes a platform intent,
guarded by the caller-side `SET_ALARM` permission — which cannot go on
`MainActivity`, because the system checks it against the Launcher too. It is
invisible, does its work, and finishes.

Two intent-filters, not one: a filter carrying a `<data>` element never matches
an intent without data, so the deeplink actions need a filter of their own.
`IntentExtrasReader` turns the `Bundle` into a plain map and guards the
unparcel, because an exported door is reachable by anything on the device and a
hostile Parcelable must not crash the launch.
2026-09-23 11:02:06 +02:00
makiolaj 15dfaf300b fix(alarm): point the next-alarm show intent at the app, not the ring screen
The `AlarmClockInfo` show intent — what the status-bar alarm icon and the
lockscreen's next-alarm line open — targeted `AlarmRingActivity` with
`CLEAR_TASK`. Tapping it opened a ring screen for an alarm that was not
ringing, which resolved to "finished" and closed itself again.

It now opens the app on its Alarms tab. That show intent is what "next-alarm
publishing" means, so the rule pinning it belongs to this milestone.
2026-09-23 11:01:54 +02:00
makiolaj 72b788b589 feat(engines): dismiss and snooze from outside, under the same lock
The verbs an assistant can ask for go on the engines, not on a handler that
orchestrates repositories behind their backs. `dismissUpcoming` and
`snooze(id, minutesOverride)` move the ring slot and the backstop;
`dismissExpired` and `dismissAllExpired` move the timer's expiry registration.
Each of those is why they belong under the engine's mutex.

A snooze duration a caller sends is clamped to one hour, because that one is
the user's own request happening now. An alarm or timer marked to delete
itself is deleted as its cycle closes rather than left disabled.
2026-09-23 11:01:54 +02:00
makiolaj 05fc6e437f feat(interop): the AlarmClock vocabulary, and a validator with no Android in it
The platform's contract as constants, and everything that decides what an
incoming intent *means* — parsed, validated and range-checked as pure Kotlin,
so hostile input can be tested without a device.

`IntentExtras` reads a map rather than a `Bundle`, because the platform's typed
getters cannot tell "absent" from "wrong type" and that distinction **is** the
validation. The range rule: clamp what the user is doing now, drop what would
define a future ring — hour 25 clamped to 23 rings at an hour nobody chose, so
it is dropped and the editor opens instead.

A link we cannot read names nothing, rather than falling back to "all of them":
a caller who mistypes one timer's id must not dismiss every expired timer the
user left standing. A repeat list sent non-empty from which nothing survives
leaves the alarm incomplete, because a repeating alarm silently becoming a
one-shot is a missed alarm next week.
2026-09-23 11:01:42 +02:00
makiolaj b43d9117ab refactor(text): lift diacritic folding out of the zone search
M8 folded text so "sao" would find São Paulo. M9 needs the same fold to match
an alarm by its label, so the fold moves to `domain/text/` and the zone search
calls it rather than owning it.
2026-09-23 11:01:42 +02:00
makiolaj 74a6295543 feat(data): schema v3 — an alarm or a timer that deletes itself after use
The `AlarmClock` contract says twice that an alarm or a timer created with
`SKIP_UI` should be removed once it has been dismissed. Without somewhere to
record that, a voice user's alarm list becomes a graveyard of one-shots nobody
asked to keep.

One column on each of `alarms` and `timers`, defaulting to 0, and a migration
that adds them. Existing rows keep the behaviour they have always had.
2026-09-23 11:01:31 +02:00
makiolaj cd26fefc69 docs: mark M8 done 2026-09-22 13:05:24 +02:00
makiolaj 36f6b356d0 docs: the world clock, its seam and its caches
`ARCHITECTURE.md` gains §16 for the tab — where the zone list comes from, why
ICU sits behind a seam, what `ZoneDirectory` caches and on what key, and why
every comparison is read at the instant rather than against a stored offset.
The module, package and rule tables count the new arrivals.
2026-09-22 13:05:24 +02:00
makiolaj 815a14235b test(arch): ICU, MaterialShapes and the ambient zone each stay where they belong
Three new rules. `android.icu` may be named only under `data/zones/`, so the
platform's name database stays behind its seam. `MaterialShapes` and
`androidx.graphics.shapes` may be named only under `ui/worldclock/`, so the
showpiece stays the one place that morphs. And nothing under `domain/` or
`ui/` may read `ZoneId.systemDefault()` or `TimeZone.getDefault()` — the
device's zone is an argument, or the tests cannot pin a single one.

All three hold retroactively for M0 through M7.
2026-09-22 13:05:18 +02:00
makiolaj 725682d851 refactor(shell): delete EmptyTabScreen
Its own KDoc said M5 to M8 would replace it. M8 was the last one: every tab
now has content of its own, so the placeholder has no caller left.
2026-09-22 13:05:18 +02:00
makiolaj 26f1effb4f feat(worldclock): the tab, the picker, and the face that answers at a glance
A hero face for home over a list of cities, each carrying its time, its offset
and how many days apart it is from you. The dial morphs between a circle and a
sun with the hour *there*: the one showpiece M0 reserved, spent on information
rather than decoration — a glance says "it is the middle of the night for them"
with no sentence needed.

The picker searches the device's own zones by word prefix. The list reorders by
drag **and** by move-up/move-down actions, because a drag-only reorder is
unreachable by a screen reader; each row reads as one sentence rather than
three texts in child order. Home is set by hand or left following the device.
At the twenty-fourth city the refusal is written on the screen, not only in the
content description.
2026-09-22 13:05:18 +02:00
makiolaj 5d11921c19 build: pin androidx.graphics:graphics-shapes
The analog face imports `androidx.graphics.shapes.Morph` directly rather than
inheriting it transitively from Material 3. Pinned to the version that already
resolved, so nothing about the resolved graph changes — only its honesty.
2026-09-22 13:05:04 +02:00
makiolaj a7a5b64137 feat(zones): ICU behind a seam, and one directory that remembers
`ZoneNames` is the seam; `IcuZoneNames` is the only file in the app allowed to
name `android.icu`, and a build rule keeps it that way. Every read is guarded —
ICU returning blank or throwing gives back null rather than a half-written
city.

`ZoneDirectory` caches the catalog, the entries and the display names, all
keyed on the locale tag, so a per-app language change re-resolves every name
instead of leaving the cities in the old language under a freshly translated
zone name. It answers in batches, so a tab with two dozen cities costs two
dispatches a tick rather than two dozen.
2026-09-22 13:05:04 +02:00
makiolaj e0d5f1f254 feat(worldclock): the catalog, the search, the comparison and the face's geometry
The pure-Kotlin half of the world clock, with no Android on it anywhere and no
ambient zone read — the device's zone arrives as an argument, and an
architecture rule now enforces that.

`ZoneCatalog` de-duplicates the device's own tzdata by canonical id and keeps
the region ids, with `UTC` as the one stated exception — and keeps an alias
whose canonical id the device does not have, because tzdata and CLDR ship as
separate modules and can disagree. `ZoneSearch` matches word prefixes over
diacritic-folded text, so "sao" finds São Paulo and "erl" does not find Berlin.
`ZoneComparison` reads the offset and the day difference **at the instant**, so
Berlin to Sydney is ten hours in January and eight in July. `AnalogFace` turns
an instant into hand angles and says whether it is day or night there.

`Zones.isValid` now tests a snapshot rather than allocating the platform's set
on every call; tzdata takes effect at reboot, so the answer cannot change under
a running process.
2026-09-22 13:04:54 +02:00
makiolaj 8dec91fd4c docs: mark M7 done 2026-09-22 10:24:12 +02:00
makiolaj cb35cc9c95 docs: the stopwatch, and why its reading is floored and banked
`ARCHITECTURE.md` gains §15 for the stopwatch and records the one correctness
find this slice turned up: a lap taken inside a segment a reboot later
discarded would have dragged the readout, the lap totals and the shade
backwards. The reading is floored at the last lap's total — and the floor is
banked, not merely displayed, or the shade's free-running chronometer walks
away from a tab that is standing still.

The package, module, receiver, service and permission tables all count one
more; §10 loses the two rows M7 closed.
2026-09-22 10:24:08 +02:00
makiolaj 83995c39db test(arch): the stopwatch stays silent, and tabular figures stay in the theme
Two new rules and two extended. The stopwatch may not name a player, a
vibrator, an audio manager — or a wake lock: it is the one timekeeper in the
app that never makes a sound and never holds the CPU awake, and that is now
a build failure rather than a promise in a comment.

`fontFeatureSettings` may appear only under `ui/theme/`, so the figure
settings stay in one place instead of spreading to call sites. The
Android-free list gains the engine; the screen's needle list gains the
service and its notifications.
2026-09-22 10:24:08 +02:00
makiolaj 5e73e81dc3 refactor(shell): the pill's stopwatch half goes through the engine
M6 left this as M7's: the pill wrote to the stopwatch repository directly
because there was no engine to ask. There is one now, so the pill's buttons
mean exactly what the tab's and the shade's mean, service transitions
included.

`LivePillSelector` is re-pointed at `StopwatchReadings` — the same extraction
M6 did for the timers — and its existing cases pass unmodified, which is the
proof that the reading moved rather than changed. A stopwatch notification
now opens the Stopwatch tab.
2026-09-22 10:24:08 +02:00
makiolaj 7b727e0d40 feat(stopwatch): the tab — the readout, the laps, and the fastest and slowest
A fixed readout over a lap list, newest first, the lap in progress counting
as its own row once there is a lap to compare it against. The fastest and
the slowest are marked in `primary` and `tertiary` — never `error`, and never
by colour alone: each carries a spoken marker so the emphasis survives being
read aloud.

The state rebuilds only what actually moves: the recorded rows and their
emphasis come off the lap table once, and the tick recomputes the hero
figure and the row in progress. Laps stop at 999 and the readout ticks only
while the stopwatch is running.
2026-09-22 10:23:54 +02:00
makiolaj 2aaae5dde8 feat(theme): tabular figures on the roles that carry a running number
M0 said the big-readout typography would be settled once there was a
stopwatch to settle it against. `ClockulaTypography` now puts `tnum` on the
display, headline and title roles, which is every role a counting number
uses, and `ClockulaReadoutDefaults` names the three sizes the readouts share.

Digits stop changing width as they change, so the timer row, the setup panel
and the live pill stop twitching too — none of them needed a call-site
change to get it.
2026-09-22 10:23:54 +02:00
makiolaj b667e46877 feat(stopwatch): the foreground service, its silent notification and the receiver
The service is alive exactly while the stopwatch is not idle, and it is
`specialUse` with the subtype spelled out — reusing `systemExempted` would
have the app claim it is continuing alarm functionality, which it is not.
No wake lock, ever: the notification carries the platform chronometer
counting up from a base derived when it is posted and never stored, so the
system draws the ticking and the process can sleep through it.

Its channel is `IMPORTANCE_LOW` because nothing here ever alerts. The Lap,
Pause, Resume and Reset buttons are broadcasts carrying no extras — the
receiver reads the stored run rather than trusting an intent — and the body
opens the Stopwatch tab. Boot, time change and package replacement all reach
the third engine now, alongside the other two.
2026-09-22 10:23:44 +02:00
makiolaj 7f8b2e79fc feat(stopwatch): the engine, its one seam and the verbs the shade can press
A third engine beside the alarms' and the timers', and a small one: four
verbs behind a single mutex, no scheduler and nothing that rings. Start,
pause, lap and reset are here rather than on the repository because three of
them arrive as notification buttons and must mean the same thing whichever
surface pressed them.

The verbs guard on the *reading's* mode, not the stored row, so a run whose
anchor a reboot invalidated resumes when the tab says Resume instead of
silently doing nothing; resuming banks the last lap's total first, so the
readout never stands still and never walks backwards. `StopwatchIntents`
namespaces the actions and hands out request codes that cannot collide with
the timers'. The service is reached through one seam, which keeps the engine
free of Android and lets the tests watch the transitions in order.
2026-09-22 10:23:36 +02:00
makiolaj 68728e5e7f feat(stopwatch): the readout, the readings and what the shade is told
The pure-Kotlin half of the stopwatch, with no Android on it anywhere.

`StopwatchFormat` splits a duration into a major field it asks `ClockFormat`
for and two truncated hundredths, so the fraction can be drawn smaller than
the seconds beside it. `StopwatchReadings` turns a stored run plus the
monotonic clock into the one reading the tab, the pill and the notification
all draw — a run whose anchor belongs to a previous boot reads paused at what
it banked, never negative and never below the last lap's total. `LapStats`
marks the fastest and the slowest, but only once three laps exist, with ties
going to the earlier lap and an all-equal set marking neither.
`StopwatchNotification` says what the shade shows without knowing what a
`Notification` is.
2026-09-22 10:23:25 +02:00
makiolaj 6ea11f7bbd docs: mark M6 done 2026-09-12 16:49:05 +02:00