A refused state change is now remembered as owed and forced by every pass
until it lands, instead of recording the opposite state, which could hide
the next real boundary. Any refusal, in the rule rewrite or the state
change, pulls the slot in to a retry, and the slot is armed in a finally
so a throwing step can't leave it unarmed. The wake-up that ends the mode
is armed allow-while-idle (one such fire a night), so Do Not Disturb no
longer lingers into the morning behind a doze deferral.
The manifest filter for the access-changed broadcast is gone: the
platform only delivers it to runtime-registered receivers, and the
screen's resume check covers it. A backup written before bedtime existed
now keeps the current bedtime settings, and a restore goes through the
engine's own field comparison, so a reminder-only difference leaves a
hand-switched mode alone.
Refs #1
Clockula can now turn on a Do Not Disturb mode of its own at bedtime and
off at the wake-up time, on the nights the user picks, with an optional
reminder before it starts. Digital Wellbeing's Bedtime mode can't be
driven by a third-party app, so this is a separate rule: a typed mode
with grayscale, dimmed wallpaper, dark theme and hidden always-on display
on Android 15+, and a plain DND schedule rule on Android 10-14.
The rule's policy always allows alarms (and media). BedtimeEngine owns one
AlarmManager slot of its own, on its own receiver and request code, and
every bedtime call on the boot, time-change and launch paths runs after
the alarm and timer passes, guarded. The rule id is never stored: the
platform lists an app's own rules, so restores and reinstalls find or
recreate it. Turning bedtime off removes the rule. The screen walks the
user through granting DND access and explains the Wellbeing overlap.
Settings are part of the JSON backup; older files restore bedtime as off.
Refs #1
The widget worked out "Today" when it was drawn, and nothing redrew it
when the date changed, so a widget drawn on Monday night still named
Tuesday on Tuesday morning. It now shares the analog faces' midnight
redraw, renamed to MidnightRefresh, and redraws on a clock or zone change.
Granting "Alarms & reminders" again left the next alarm on the inexact
fallback until something else re-registered it. The system's permission
broadcast now reschedules alarms and timers.
SystemUI launches a full-screen intent only when a notification is first
posted, never on an update. The alarm service went foreground with a
placeholder that had none and added it in the update, so a locked phone
could ring behind a black screen. The placeholder now carries it.
Timers had no full-screen screen at all. A finished timer now opens
TimerRingActivity over the lock screen with Stop and +1 min, posted
under a second notification id so the expiry is a fresh post. It only
does so while the timer is sounding, so it never covers a ringing
alarm's screen or wakes the display for an already auto-silenced timer.
Fixes#15
Every widget instance can now follow the system or be pinned to Light or
Dark, and on Android 12+ turn the wallpaper's colours off in favour of
the app's own palette. Both are set in the widget's config screen and
stored in its own Glance state; an instance without them keeps today's
look.
Glance content takes them through WidgetTheme. The plain-RemoteViews
parts (the analog and digital faces, readouts and chronometers) go
through a new WidgetPalette, which resolves the widget_* palette under a
forced day/night and sets concrete colours, or day/night pairs, on the
views. The app's own tones are now widget_app_*, so they stay reachable
on API 31+. The analog and digital clocks are re-coloured on API 31+
only: below that AnalogClock's hands and a shape's background tint can't
be set through RemoteViews, so their config screens don't offer it.
The next-alarm and timer widgets gain a config screen for this
(configuration_optional, so placing one doesn't stop there). The analog,
stopwatch and quick-timer config screens become settings screens with an
OK, like the digital clock's, since they now hold more than one choice.
Background opacity is left out: most faces are a shape with text in its
container colour rather than a card, so there is no background to fade
without losing contrast against the wallpaper.
Closes#6
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.
"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.
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.
- 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.
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.
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