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.
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>
`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.
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.
One AlarmManager slot for every timer, registered on ELAPSED_REALTIME_WAKEUP
so the clock the domain anchors on is the clock the platform wakes on. Its
fire carries no id and is an idempotent sweep, backed up by a second trigger
inside the service, because neither has to be reliable on its own.
The foreground service posts per state change rather than per second — the
platform chronometer draws the countdown — and its actions are broadcasts, so
pause and reset still work with the process dead. Every verb is guarded
against a stale id.
Expiry reuses the alarm's audio path: the same player, vibrator, source policy
and fallback-to-vibration chain, with a zero ramp and its own channel. No
full-screen intent, no challenge, no snooze — a timer is not an alarm. Two
timers expiring together share one ring; a timer expiring after a previous
one's auto-silence window lapsed gets a sound of its own, which is the whole
point of having a timer.
A systemExempted foreground service holds the ring: audio focus, USAGE_ALARM,
the volume ramp, vibration, and the ringtone URI. It goes foreground
synchronously before it reads anything from the database, because the platform
kills a service that reaches startForeground late — including on the path where
there turns out to be no session to ring.
The chain that matters is the one that never ends in silence. A full-screen
intent when canUseFullScreenIntent() allows it; a high-priority heads-up
notification that still rings when it does not; the device's default alarm
ringtone when the configured URI will not open; and forced vibration when no
audio source will play at all.
Preparing a MediaPlayer is done off the main thread — a synchronous prepare at
the moment the alarm fires is an ANR — and a player is released rather than
orphaned on every failure path, so a dismissal cannot be outlived by the ring
it cancelled.
The ring screen here is plumbing and a placeholder: window flags to show over
the lockscreen and turn the screen on. Its UI is M4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clockula's seed is #6B7A5C, the third step of the family's colour cycle.
Calendula's slate (#5C6B7A) and Agendula's mauve (#7A5C6B) are the same three
bytes rotated — and that byte rotation is exactly a 120° hue rotation, so the
three seeds sit at 210°, 330° and 90° with identical saturation and lightness.
The fallback schemes are therefore not eyeballed: they are Agendula's
hand-picked scheme with its hue rotated the same 120°, which keeps the three
apps structurally one palette turned rather than three colours picked. The
cycle closes at three.
The launcher icon keeps the family's Calendula bloom badge path-for-path and
swaps the task card for a clock dial, drawn as a 308° arc so the badge
overhangs into a gap rather than colliding with the stroke — the same device
Agendula uses to open its card's corner.
MainActivity is M0 scaffolding: theme plus the crash surface, with a
placeholder where the four-tab shell and the live pill land in M4. The theme
follows stored preferences from M2, once there are any.
The manifest declares no permissions yet, deliberately. Clockula's permission
set belongs to the alarm engine and is declared in M3 alongside the receivers
and the ringing service that need it, so the manifest never claims a
capability the code cannot honour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L94fydiJC37LtxVusNQBDy