A full-screen intent is only a heads-up on a phone in use. A new, opt-in
setting under Alarms lets a ringing alarm or timer open its screen over
the app in use instead. It relies on "display over other apps", which
turning the setting on sends the user to grant; the switch reads as off
while the permission is missing. A locked or dark phone is still left
to the full-screen intent, so the screen never opens twice.
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
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
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
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 settings screen Android's screen saver list opens gets a task of its
own and stays out of recents, so it never lands in or clears the app's
task, and its bars follow the app's theme. The on-demand screen saver is
noHistory, so leaving it by Home does not bring the app back to it later,
and it only touches the keep-screen-on flag when charging starts or stops.
The next-alarm line uses the same 12/24-hour rule as the big readout,
joined to the day by a translatable string; the minute's crossfade shows
the old minute fading out rather than two copies of the new one.
"Next alarm" reads the next-alarm widget's model and shows the weekday
and time of the next alarm, or "No alarms"; a tap opens the Alarms tab,
behind an unlock on a locked device, through the PendingIntent overload
of startActivityAndCollapse on API 34+.
"Quick timer" is the quick-timer widget's twin: at rest it shows its
duration and a tap starts a transient timer through the timer engine;
while that timer is active the tile is on, shows when it ends (or that
it is paused or done), and a tap resets it. It works on a locked device,
as the timer notification does. Its duration is a new backed-up setting,
chosen by long-pressing the tile, with the widget's bounds and picker.
Both are active tiles, redrawn by TileSync when the state they mirror
changes rather than bound on every panel open. No new permission.
Fixes#26
A DreamService shows a full-screen clock, the date and the next alarm,
digital or as the world clock's analog face, on black. Night mode dims
every colour towards black and turns the display down; the clock moves
to a new spot each minute so nothing burns in. It wakes only on the
minute and on a time, zone or 12/24-hour change.
The tabs' settings gear becomes an overflow menu with "Screen saver" and
"Settings". An app cannot start the system's dream without a system
permission, so that item opens an activity with the same content, which
keeps the screen on only while charging. Style and night mode live in
Settings > Clock and in the dream's own settings screen, and are carried
in backups; older backups restore the defaults.
Fixes#25
"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.
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.
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
The local-time row copied the saved row's name, summary and time
layout. Both now draw it through one RowText, so a change to how a
row looks lands on both. The Settings hub's Clock entry also names the
new switch, so it can be found from the list.
Issue #24 asks for Google Clock's "automatic home clock": a temporary
Home row while the device is somewhere the time differs from home. In
Google Clock that works because the hero clock is local time. Clockula
is the other way round: the hero face is home (the stored home zone, or
the device's zone when none is stored), every row is read against home,
and with a stored home the face already shows home time on a trip. A
Home row would only repeat the face; what goes missing while travelling
is the local time.
So the feature is turned round. With a home zone stored and the device's
zone on a different UTC offset right now, the tab shows the device's
zone as a temporary "Local time" row between the face and the saved
cities. It is never stored, sits outside the reorderable column, has no
move or remove actions and does not count towards the 24-city cap. The
comparison is the current UTC offset, as Google's is, so Paris with
home in Berlin shows nothing. The row is hidden when a saved city is
already on the device's offset. It is rebuilt on every tick from the
same device-zone read the face uses, so it appears and disappears
within a tick of a time-zone change while the tab is open.
Home that follows the device can never differ from it. Settings > Clock
gets an "Automatic local clock" switch (on by default) under the home
zone row; while home follows the device the row is dimmed and its hint
says it needs a home time zone. The switch is stored as
automatic_local_clock and included in backups. Files written before this
change restore it as on.
Fixes#24
An alarm card swipes away towards the end edge, or deletes from its
"Delete" accessibility action, and the list's snackbar offers Undo.
The editor's Delete drops its confirmation for the same Undo, routed
through a singleton so the list can answer it after the editor pops.
Timer cards get the same Undo for their corner delete.
AlarmEngine.delete stops a ringing alarm and clears a snoozed one's
notification under the engine lock; restore puts the row back under
its own id with its ring state and reschedules. A ringing alarm comes
back dismissed, never ringing; a finished timer comes back reset.
The swipe is hand-rolled so the end-to-start drag stays with the tab
pager.
Fixes#20
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
A timer ring that hands over from one expired timer to another now starts at full volume instead of ramping again from near silence.
The timer-vibrate migration only treats an install as fresh when the package was never updated and the store is empty. An updated install whose store was replaced after corruption, or an Auto Backup restore onto a new device, keeps the alarm default as before.
The Vibrate row now says whether it is on, and only when it is off adds that a silent timer still vibrates.
Refs #22
A ringing alarm and a finished timer used to stop after a fixed ten
minutes. Settings > Alarms and Settings > Timers now each have a "Silence
after" row with Google Clock's choices: 1, 5, 10, 15, 20 or 25 minutes,
or Never. Ten minutes stays the default, so nothing changes on upgrade.
The value is app-wide; a per-alarm override would need a new column, a
migration and an editor row, and is left out.
"Never" rings for an hour, then stops. The backstop is also what keeps a
phone left at home from ringing its battery flat, what bounds the ring
services' wake locks, and what stops a reboot hours later from resuming a
ring nobody was there for. A truly unbounded ring would give all of that
up, so Never maps to a one-hour hard stop, longer than any finite choice,
and the settings hint says so. Stored values clamp to 0..60 on read, on
write and from a backup.
The window is read everywhere the fixed constant was: the resolver's
ringing and late-snooze checks (which now take it without a default), the
backstop onFire arms, how late a snooze fire may arrive, whether an
interrupted ring is resumed after a reboot, upcoming(), the timer ring
policy, the lapsed-session check and the timer ring screen. Both ring
services' wake-lock timeouts now derive from the hard stop instead of a
hard-coded fifteen minutes.
When the resolver closes a ring (the window was shortened mid-ring, the
clock jumped, the alarm was switched off), the engine now also stops the
ring service and cancels the backstop. Before, it only cleared
ringingSince and left the sound to a backstop armed for the old window.
Both values are backed up as default_alarm_silence_after_minutes and
default_timer_silence_after_minutes; a file without them imports as ten
minutes.
Fixes#17
After a user action in the app sets an alarm, a snackbar says how long
until it rings: "Alarm set for 7 hours and 12 minutes from now". The row
already said "in 7h 12m", but nothing drew the eye to it, so an AM/PM or
wrong-day slip went unnoticed until the morning.
It is raised by switching an alarm on in the list, and in the editor by
picking a time (or keeping a new alarm's default one), arming or
disarming the skip, and changing the repeat days. Day taps wait a second
to settle, so Mon-Fri is one snackbar rather than five; a newer
countdown always replaces the one on screen. Switching the announced
alarm back off takes it back. A switched-off, ringing or snoozed alarm
says nothing, and the AlarmClock intents never reach it, since those
already get the status-bar icon.
AlarmSetCountdown decides what it says, purely, with the row's rounding;
the engine gains upcomingFor(), a read-only resolve of one alarm.
Fixes#19
Timers vibrated by the alarm vibration default and never ramped. Both are
now the timers' own app-wide settings, next to the default timer sound in
Settings > Timers: a Vibrate switch and the same Gradual volume picker
alarms use. Timer.resolveSettings reads them instead of the alarm default
and the old TimerRing.RAMP_SECONDS constant.
A fresh install starts with timer vibration off, as Google Clock does, and
no ramp. An existing install keeps what its timers did: a one-shot DataStore
migration writes the new key from the alarm vibration default before the
first read, so nobody's kitchen timer silently stops buzzing on update. A
fresh install is told apart by an empty store (anything that has run once
has written its boot id); the migration writes the default there too, so it
never runs twice.
Backups carry both new fields. A file without them restores the alarm
vibration default as the timers' (what that install's timers did) and a
flat ring.
Silent plus vibration off still signals: Silent gives the player no
sources, and its onSilenced callback starts the vibrator whatever the
setting says. The settings row says so.
Fixes#22
One switch in Settings > Clock adds seconds to the home readout and
every city row, in the same 12/24-hour and locale form as before, and
gives the analog face its second hand. Off by default.
The tab now ticks on the wall-clock boundary through a new
Ticker.alignedTicks: once a minute with seconds off, once a second only
with them on, replacing the free-running 200 ms tick. The setting is in
backups as world_clock_show_seconds; older files import with it off.
Fixes#23
The snackbar appended the codec's or the exception's own English text,
class names and all. Failures now carry a category (not a backup, from a
newer version, invalid data, too large, unreadable) that the screen maps
to a string resource; the detail stays in the result for tests.
The system reports only the earliest alarm clock of any app, so an older
alarm left in the stock clock showed up as a problem the offered fix
couldn't solve. Another app's alarm at or before Clockula's next fire is
now fine; a missing registration, or anything later than ours, still is
a problem.
The sound Google Clock and BlackyHawky's Clock use for timers is
timer_expire.ogg from AOSP DeskClock, bundled with those apps rather
than installed as a system ringtone, so RingtoneManager never lists it.
It is now bundled here too and listed first in every sound picker.
The choice is stored as clockula://tone/timer_expired and resolved to
the raw resource when played, so it survives a restore into another
build variant. The file is Apache-2.0, recorded in docs/PROVENANCE.md
with the licence text in licenses/.
Fixes#14
The quick-timer config screen now ends its list of presets with a
"Custom…" row that opens the timer keypad. Anything from 1 second to
59:59 is accepted; past that the keypad shows a range hint and OK stays
disabled, since the widget's readout is sized for MM:SS. A stored value
that isn't a preset marks the Custom row, with the value as its summary.
The keypad dialog is lifted out of the timer editor into a shared
TimerDurationDialog, so both use the same one; the editor's now also
shows the range hint the setup screen shows.
Closes#7
Tapping the digital or analog clock widget now opens Alarms instead of
World clock, and both config screens gain an "On tap" setting (Alarms,
Timers, Stopwatch, World clock) stored per instance next to the style.
Instances placed before the setting existed open Alarms. The analog
config becomes a settings screen like the digital one, with the style
behind a row, to make room for it.
A widget tap sometimes showed whatever the app was last on instead of
its tab, for two reasons. The shell replayed a deep link as a tab tap
with restoreState, so it restored the tab's saved back stack, nested
screen and all (fixed by the pager shell, which pops to the tab). And
the widget intents carried only NEW_TASK, so with a picker or settings
page left on top of MainActivity the task was merely brought forward and
onNewIntent never ran. Every widget that opens the app now builds its
intent in one place with CLEAR_TOP and SINGLE_TOP as well.
Closes#8
The World clock tab now prints the home time under the hero face in the
app's 12/24-hour format, reversing the earlier call to leave it out.
Reading the exact minute off a dial is slow, and that minute is what
most visits to the tab are for.
The face no longer has a content description, so TalkBack does not read
the time twice. The readout, "Home", the city and the zone name are one
merged node instead.
Closes#4
Settings > Timers offered a "Default duration", but nothing that creates
a timer ever read it: the keypad starts empty. The row, its preference,
ClockDefaults.timerDuration and the backup field are gone, and the
screen's hint and the hub's subtitle now only speak of the sound.
Backups no longer write default_timer_duration_millis. Older files that
carry it still import, since the codec ignores unknown keys.
Closes#5
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.
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.
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.
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.