The countdown used the stock Material snackbar, while Agendula and
Calendula confirm with floret-kit's SnackChip. It now opens as that chip:
bottom-start in the FAB's band on the list, ellipsising short of the FAB,
and in the editor's snackbar slot so it follows the predictive-back
preview. It fills the band's height, as an Undo chip does in the other
apps, and sits on the secondary container to stand apart from the cards.
The chip has no timer, so AlarmSetChipBand holds it for a short
snackbar's four seconds, stretched as the accessibility settings ask.
The hand-over is unchanged: a chip cut short stays pending for the next
resumed screen.
Beside the FAB there is room for about 308 dp, so the message is now
"Alarm in 7 hours and 12 minutes" rather than "Alarm set for … from now",
which would have lost its number to the ellipsis.
Refs #19
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
A removal the system refuses while turning bedtime off now keeps the slot
armed fifteen minutes out as its retry, instead of cancelling it and
leaving Do Not Disturb on until some unrelated launch. A refused state
change records the opposite state so the next pass retries it, and a
rule that was rewritten (which clears its condition on Android 10-14) is
switched regardless. A refused rewrite is remembered and retried by every
pass until it lands, rather than resting on the one edit that asked.
Refs #1
Without exact-alarm access the slot falls back to a ten-minute window
instead of plain set(), whose slack could end an eight-hour night hours
late. A freshly opened bedtime screen reads whether bedtime is on from the
store, not from its still-loading state, so access granted while the
screen was closed creates the rule. A restore that rolled back no longer
forces the mode.
Refs #1
Android rations allow-while-idle wake-ups per app in doze, and the timer's
expiry slot and the alarm's fallback share that ration, so a 07:00
wake-up fire could hold back a timer due at 07:04. The bedtime slot is
now a plain exact alarm that doze may defer, which can move a mode
boundary but never a ring. A restore of unchanged bedtime settings no
longer forces the mode, and the screen skips its rule check while bedtime
is off.
Refs #1
Only a bedtime before 06:00 now counts as the small hours after the
chosen evening, so a day sleeper's 08:00 bedtime runs on the days picked.
A rule whose removal was refused while turning bedtime off in the daytime
is removed by the next pass, and a restored reminder length the picker
doesn't offer stays choosable.
Refs #1
A bedtime before noon now belongs to the evening before, so Sunday to
Thursday nights at 00:30 run early Monday to early Friday, not Saturday
night through Wednesday night. Turning bedtime off clears the "last
applied" state only once the rule is really gone, so a refused removal
is retried instead of leaving Do Not Disturb on. Reopening the screen
after granting access elsewhere creates the rule even if the system's
broadcast never arrived. The empty-mask rule lives in one place.
Refs #1
Whether the wake time is the same night or the next morning is now
decided on the local times, so a bedtime in a spring-forward gap no
longer turns a 45-minute window into a day-long one. Screen edits start
undispatched so they reach the engine's lock in the order they were made,
and an edit that rewrites the rule also re-asserts the mode, in case the
platform reset its state.
The launch-time bedtime pass runs even if an earlier engine pass failed,
a restore that never applied settings no longer forces the mode, and a
user who never turned bedtime on pays nothing for it at launch or boot.
The screen says when equal bedtime and wake-up times mean it never starts.
Refs #1
A rule recreated by a pass (deleted in Settings, access re-granted) now
gets its state set even when no boundary was crossed, and the applied
state is recorded only once the system accepted it. Edits that can't move
a boundary (reminder, effects) no longer re-assert the mode over a manual
override, and the screen's writes run on the application scope so leaving
right after a change can't cut it short. A late reminder counts the
minutes actually left, and one slot fire reads the zone once.
An equal bedtime and wake time is now no window instead of a day-long one,
which on every night meant Do Not Disturb never ended, and an empty days
mask in prefs reads as every night, as a backup's does. The rule-id extras
are translated to the bedtime action in MainActivity, keeping the routing
table keyed on the action alone.
Refs #1
Settings opens a rule's configuration activity with no action, only the
component and the rule id, so MainActivity now routes on the rule-id
extras. Each engine pass reads the clock once, so the mode's state and the
next slot can't disagree around a boundary, and non-edit passes switch the
mode only when the schedule crossed a boundary since Clockula last set it
(persisted), which also catches up a wake-up missed during a force stop
while leaving a mode switched on by hand alone. A reminder fire that also
ends a window now turns the mode off, and a reminder the schedule moved
away from is withdrawn.
The rule is only rewritten when a field it carries changes, and a rewrite
keeps the existing policy if it still allows alarms, so Android 10-14 no
longer loses the user's Do Not Disturb edits. The bedtime receiver, screen
and fallback alarm call are guarded, the boot-path resync runs even if an
earlier engine failed, and a backup with no nights restores every night.
Refs #1
Picking "Timer expired" as the default alarm or timer sound showed "Custom" in Settings, because the summary knew only the device default and silence. It now names the bundled sound, as the editors already do.
ARCHITECTURE.md now names the exact bundled sentinel instead of a prefix the code does not recognise, lists clockula://media values in the alarm column, gives the timer column the same values, and says RingtoneCatalog lists the bundled sounds too.
Refs #14
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
ARCHITECTURE.md still said every non-silent ringtone_uri is a content URI and listed the picker's rows without the bundled "Timer expired" sound, so a reader could Uri.parse the clockula://tone/ sentinel directly. It now names the sentinel, BundledTones as its only resolver, and where the tone sits in the picker.
A view-model test now covers a device whose only listed tone is the bundled one, which must still show the "no alarm sounds" banner. The existing test only checked an empty list, so it passed even if the bundled tone were counted as a device sound.
Refs #14
A new setting in Settings > Alarms: while the ring screen shows, the
volume buttons change the volume (the default, and what they did
before), snooze, dismiss, or do nothing. It is app-wide, stored next to
the clock defaults, and included in the backup; a backup without it
restores "change the volume".
A press is made without looking, so VolumeButtonPolicy keeps it from
ending an alarm more finally than the ring screen would. Dismiss with a
dismiss challenge set snoozes instead, so the challenge still stands.
With no snoozes left, Snooze (and Dismiss behind a challenge) falls
back to changing the volume, never to the engine's refused-snooze path,
which dismisses. The key's snooze also asks the engine to keep ringing
if the limit turns out to be reached after all (AlarmEngine.snooze's new
dismissIfRefused). The ring screen decides whether a press is its own
on key down and keeps that for the release, leaves the keys alone once
another alarm has taken over the ring, and ignores a second snooze or
dismiss while the first is still running.
Only the ring screen handles the keys. While the ring is a heads-up over
another app, a background app gets no key events, and the one way
around that does not hold up: a remote-playback MediaSession whose
VolumeProvider takes the keys. On Android 13 and later
MediaSessionRecord.canHandleVolumeKey refuses such a session from an app
without a MediaRouter2 routing session, which Clockula's would be, so on
current Pixels the keys never reach it. Where it does work, it loses to
whatever app is in front or played last, shows a cast-volume slider and
a phantom media player, and takes media buttons while the alarm rings.
AOSP DeskClock and BlackyHawky's Clock also handle the keys in their
alarm activity only. ARCHITECTURE.md records the limitation.
Fixes#18
The notice is local-only, so its Dismiss is never bridged to a watch where the phone's unlock does not apply. The closing and skip-spending checks land a second past the fire grace, since onFire still accepts a fire at exactly the grace.
Fixes#16
The list and editor now format a date through the same helper as the
widget, and the date pickers keep their default years unless a stored
date lies beyond them, instead of always spanning to 9999.
The upcoming check that spends a skip dismissed in advance now wakes once the skipped occurrence's fire grace is over rather than at the occurrence itself, so it can never get ahead of a fire still in flight for it.
Fixes#16
Keeping a skip armed through its fire grace let the editor's switch or another dismissal inside those two minutes ring the skipped alarm late, or be absorbed. The resolver goes back to spending the skip at its occurrence and recording it as handled, and the upcoming check wakes at an armed skip's occurrence so the list stops saying "skipping next" on time.
Fixes#16
The resolver now keeps a skip armed until its occurrence is out of the fire grace too, so a reschedule in those two minutes cannot spend it early and let a fire already in flight ring the dismissed alarm; onFire refuses that fire by the armed watermark. The notifier also cancels notices it posted itself, which the platform may not list as active yet.
Fixes#16
"Paused, back on" named the day after the pause, which for a weekday
alarm can be a Saturday it never rings on. A pause under way now reads
"Paused through Wed, Oct 28".
Picking a date on a switched-off alarm switches it on, the pause picker
cannot confirm a range that is already over, a spent pause stored with
its ends reversed is cleared too, and the widget formats its date in the
app's own locale.
The upcoming check now also wakes once a noticed occurrence's fire grace has run out, so a notice cannot outlive a late fire, and once an armed skip's has, so the list stops saying "skipping next" about an occurrence already gone. onFire is back to checking only the armed skip: the resolver deliberately re-arms a handled occurrence that a clock moved backwards put back in the future, and that one must ring.
Fixes#16
Moving the time or changing the days left a pending skip pointing at the
old sequence, so the skipped day could ring at its new time. Every editor
change that can move a fire time now re-arms the skip.
A pause under way now reads "Paused, back on Thu, Oct 29" instead of
naming its last day, which read as the day it ends before. Reopening a
begun pause keeps its start, the date pickers take their bounds from the
clock when they open, and the dismiss chooser shows the scheduled date and
pause like the list does.
The Dismiss action's unlock requirement is honoured only from Android 12, so on 10 and 11 the notice is now secret on the secure lock screen rather than dismissible by whoever holds the phone.
Fixes#16
A natural fire whose instant equals the skip or the handled-occurrence watermark is ignored, so a dismissal racing a reschedule cannot ring. Dismissing a repeating alarm persists its resolution before arming the skip, so an unspent old skip cannot absorb it. The deferred notice sync runs even if starting the ring throws, the swipe record is one synchronised read-modify-write, and it stays out of backups.
Fixes#16
A skip armed under one pause kept its watermark when the pause changed,
so removing the pause left the alarm silent until the old pause's end.
The engine now re-arms a pending skip after a pause edit.
Spent dates and pauses are cleared with targeted, conditional updates
instead of a whole-row rewrite. SET_ALARM ignores a date or pause that
has already gone by when looking for an alarm to re-use, the editor
re-reads the clock when a date picker opens, and the next-alarm widget
shows the year for a date in another year.
A skip spent on its occurrence now becomes the handled-occurrence watermark, so a second resolution inside the fire grace cannot ring the skipped alarm after all. The upcoming notice records a swipe through its delete intent rather than inferring one, so a reboot or a blocked channel no longer hides it for good; its Dismiss asks for an unlock, and a Dismiss for a switched-off alarm clears its notice.
Fixes#16
A one-shot alarm can now ring on a chosen date, and a repeating alarm
can be paused for a range of days. Both are local dates: the date is a
floor and the pause a hole in the occurrence sequence, so DST and zone
changes resolve as for any other alarm. A missed date behaves like a
missed one-shot, and each clears itself once it can no longer change a
candidate. Picking a repeat day clears the date, as in Google Clock;
"skip next" stays the quick version of a pause.
The editor uses the M3 date and date-range pickers, which offer no past
day. The list, the next-alarm widget and the self-check read the
resolver, so they follow. The schema goes to v4 with a migration, and
the backup format to v2, which still imports v1 files. SET_ALARM no
longer re-uses an alarm that carries a date or a pause.
Fixes#21
A fire broadcast already in flight when a skip was armed on its occurrence is now ignored, and a Dismiss that lands once the occurrence is due is stale. The notices are synced after the ring starts on the fire path, and a swiped-away notice stays gone across a process restart. A switched-off alarm answers NotFound.
Fixes#16
Two hours before a natural occurrence (AOSP DeskClock's lead), a silent
"Upcoming alarm" notification offers Dismiss, and the alarm's row shows
the same Dismiss as a chip. Dismissing is dismissUpcoming's own branches:
a repeating alarm skips that occurrence, a one-shot is switched off.
The notices are synced at the end of every reschedule, after the real
registration, and a third AlarmManager slot on its own request code
wakes when the next window opens. The sync is wrapped so a notification
failure can never stop a ring. Dismiss names the occurrence and is
refused once the alarm is ringing or has moved, so it can't get round
the dismiss challenge. No challenge is asked before the ring, as in
Google Clock.
Fixes#16
The no-re-emission check now waits for the write to be readable before
asserting, and the seconds switch has its own settings test instead of
riding in the clock-defaults one. WorldClockSource.state takes its
refresh signal without a default, and ARCHITECTURE.md's slice count
and alignedTicks guarantee match the code.
The fitted seconds size keeps five percent spare, since hinted glyph
advances do not scale exactly. The receiver no longer resets the
process's default zone: that was speculative, and it changed global
state from a UI flow. The settings state takes the seconds flag without
a default, and a garbled KDoc sentence is fixed.
A stored value of 60 minutes or more now reads as Never, which rings that
long anyway. The picker no longer shows both "60 minutes" and "Never" for
the same window. Also drops a redundant null check in TimerRingPolicy.
Refs #17
Telling a fresh install from an updated one (package install times, the boot id, a string-encoded tri-state) was more machinery than the default is worth. Timer vibration is a plain boolean again, defaulting to on like the alarm default, and a startup copy writes the alarm default into it once, so no install's timers change on update. The copy runs in its own coroutine, off the engines' boot pass.
Fresh installs start with timer vibration on, matching alarms; the switch is one tap away in Settings > Timers.
Refs #22
Fifth round of review fixes for the silence-after setting.
A ring whose start the clock has moved back behind is now re-anchored to
now, once, and stored. Before, the negative age lingered, and every later
re-measure gave the ring a fresh window. With that fixed, onAutoSilence
can safely re-arm again when the ring is still inside the window in
force, so a backstop queued behind a raised setting no longer ends the
ring early.
The timers' setting is now written through
TimerEngine.setTimerSilenceAfter, which records a session that has
already outrun the old window as lapsed, so raising the setting cannot
restart a timer that went quiet while no service pass was running. The
lapse rule and the lapsed anchor live in TimerRingPolicy, shared by the
policy and the engine.
Refs #17
The announcer now records only which alarm a user action set, and when.
What the snackbar says is derived from upcoming() while a screen shows
it, re-subscribed per announcement so the first read is from after the
write. That removes the second resolve path through the engine lock
(upcomingFor is gone), the publish lock, and the flash of a pre-write
countdown. The engine is observed only while an announcement stands.
Freshness is judged when a screen picks the announcement up, so one
already showing is not cut off half-way. Consuming matches a sequence
number. Latest action wins across alarms again: the sentence names no
alarm, so a settling announcement must never land on top of a newer
action on another one. Switching an alarm off announces nothing, and a
dismissal retracts before it skips.
Refs #19
The row's time is fitted only with seconds on, against both day periods
since an AM marker can be the wider one, and the style is built once by
the screen rather than per row. With seconds off the row is exactly as
before. The home readout stays on one line with seconds, shrinking to
fit instead of wrapping. SCREEN_ON is dropped: the tab cannot be seen
again without a resume, which already refreshes. §5 no longer claims a
clock change heals within one tick for free.
Fourth round of review fixes for the silence-after setting.
onAutoSilence no longer re-arms a ring it measures as inside the window.
After a clock moved back, that check could push the backstop out again
and stretch the ring well past its window. Every writer of the window and
every clock change already re-arms the backstop under the engine lock, so
a backstop that fires has had its window, and it closes the ring as in v1.
A one-shot alarm the resolver switches off as missed also clears a
pending skip flag, as the resolver's other one-shot disable does.
SNOOZE_GRACE is its own ten-minute constant again, not tied to the
setting's default.
Refs #17
An empty store was a racy sign of a fresh install: the UI can write the store (the notification-permission flag) before the startup settle runs, and a fresh install would then vibrate its timers. The boot id is written only by the engines' pass, which runs after the settle in the same coroutine, and an Auto Backup restore brings the old device's.
Once settled, the startup pass costs one read and no package-manager call. A garbled stored value is replaced rather than left following the alarm default.
Refs #22
With the tick a minute long, a language change left the city names in
the old language until the next minute; the screen now sends a refresh
on every resume. The row's time is sized once per format against a
wide sample instead of searched for every second, and ellipsizes as a
last resort rather than clipping. BackupSettings takes the setting
without a default, so a snapshot cannot drop it silently.
Third round of review fixes for the silence-after setting.
A backstop that fires while the clock is still behind the ring's start
now closes the ring instead of re-arming in a loop until the wall clock
catches up. A clock change re-measures a ring in progress before it
reschedules: a clock moved back re-arms the backstop at most one window
from the moved clock, and one moved forward past the window closes the
ring.
The engine now owns the alarm setting's write. setAlarmSilenceAfter
stores it and re-measures the ring under one lock and without
cancellation, so a write can no longer land without the re-measure.
A snooze the resolver drops takes its "Snoozed until" notification with
it. SNOOZE_GRACE is derived from the default window, and the silence-after
picker moves to ui/settings, since it is not a per-alarm override.
Refs #17
A notice is now re-checked against the engine before a screen shows it,
so an alarm switched off or deleted by any path - the ring screen, a
notification, an intent, a restore - is never announced as set. A
retract takes the publish lock, so a publish already past its read
cannot put the notice back.
Settling is per alarm: switching one alarm off in the list no longer
cancels the repeat days still settling on another. The identical-words
dedupe is gone, since it could swallow a real confirmation.
The editor's writes that move a fire time, with their reschedule and
announcement, run on the application scope. Pick, OK, back could
otherwise cancel the reschedule half-way and leave a stored time the
system had not been given. upcomingFor() reads alarm and state under
the engine lock, so a fire cannot land between the two reads.
Refs #19