Commit Graph
90 Commits
Author SHA1 Message Date
makiolaj a186e5afcd Merge branch 'feat/19-alarm-set-snackbar' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmsScreen.kt
#	docs/ARCHITECTURE.md
2026-10-26 20:50:55 +01:00
makiolaj d81cd04b60 Merge branch 'feat/18-volume-buttons' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/alarm/AlarmEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/AlarmDefaultsScreen.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/ARCHITECTURE.md
#	docs/BACKUP.md
2026-10-26 20:49:43 +01:00
makiolaj 333a23d6be Merge branch 'feat/17-silence-after' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/alarm/AlarmEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/BACKUP.md
2026-10-26 20:48:54 +01:00
makiolaj 440d098c77 Merge branch 'feat/16-upcoming-alarm-notification' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt
2026-10-26 20:48:13 +01:00
makiolaj ea8e159b39 Merge branch 'feat/1-bedtime-mode' into release/v1.0.0
# Conflicts:
#	docs/ARCHITECTURE.md
2026-10-26 20:47:54 +01:00
makiolaj 6aa046515c Merge branch 'docs/27-wear-os-note' into release/v1.0.0 2026-10-26 20:47:36 +01:00
makiolaj 2191166035 fix(alarms): show the alarm-set countdown as the family's SnackChip
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
2026-10-26 20:21:56 +01:00
makiolaj 817fe77b17 fix(bedtime): record only accepted states, urgent wake-up, keep bedtime on old restores
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
2026-10-22 12:54:02 +02:00
makiolaj 74c0c278ef fix(bedtime): retry refused removals, state changes and rule rewrites
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
2026-10-22 12:41:15 +02:00
makiolaj 131a28c1be fix(bedtime): bounded fallback window, fresh-screen access check
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
2026-10-22 12:30:26 +02:00
makiolaj 1a5552a221 fix(bedtime): keep the slot off the allow-while-idle ration
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
2026-10-22 12:20:25 +02:00
makiolaj 8b182c999b fix(bedtime): small hours end at 06:00, retry daytime removals
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
2026-10-22 12:11:04 +02:00
makiolaj b701859156 fix(bedtime): name nights by their evening, retry a refused removal
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
2026-10-22 12:02:39 +02:00
makiolaj 1a443c066f fix(bedtime): DST-safe windows, ordered edits, quieter passes
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
2026-10-22 11:53:39 +02:00
makiolaj 2dce78281a fix(bedtime): switch a recreated rule, keep manual overrides, finish edits
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
2026-10-22 11:38:56 +02:00
makiolaj 119197106b fix(bedtime): follow boundaries, open from Settings, keep the user's policy
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
2026-10-22 11:29:33 +02:00
makiolaj 83a77c1bb0 fix(settings): name the bundled sound in the default-sound rows
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
2026-10-22 11:20:39 +02:00
makiolaj 69b436cffd feat(bedtime): add an opt-in bedtime mode under Settings
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
2026-10-22 11:19:38 +02:00
makiolaj bdcf780028 docs(roadmap): rewrap the Wear OS note
Refs #27
2026-10-22 11:15:52 +02:00
makiolaj 1559e68b2f docs(ringtones): describe the bundled tone and test its no-sounds banner
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
2026-10-22 11:14:42 +02:00
makiolaj 2927585608 docs(roadmap): name both data paths and F-Droid's real response
Refs #27
2026-10-22 11:13:55 +02:00
makiolaj b8968f5e19 docs(roadmap): settle the Wear OS note on what the docs say
Keep §5's own claim that only crash reports leave the phone, say the
dependency would ship in every channel, and keep the standalone app's
distribution caveat.

Refs #27
2026-10-22 11:12:18 +02:00
makiolaj dc23618c56 docs(roadmap): drop the AlarmClock route from the Wear OS note
AlarmClock intents only reach apps on the same device and can't read
state, so they are no route for a watch. Cite PRIVACY.md §4 and §5
together and say "no product flavours" precisely.

Refs #27
2026-10-22 11:10:38 +02:00
makiolaj da16ab23cc docs(roadmap): quote the policy and list the AlarmClock route for Wear OS
Cite PRIVACY.md's own wording, name F-Droid's non-free-dependency rule,
and note that third-party watch apps can already drive Clockula through
the AlarmClock intents.

Refs #27
2026-10-22 11:08:53 +02:00
makiolaj 99c052f94c docs(roadmap): state the Wear OS blocker against the policy as written
PRIVACY.md already documents Auto Backup and crash reports as ways data
can leave the phone, and there is a single build rather than an F-Droid
one. Say that, and call the entry blocked rather than unscoped.

Refs #27
2026-10-22 11:07:07 +02:00
makiolaj fe1c2a3d48 docs(roadmap): reduce the Wear OS note to the decision it needs
Any phone-to-watch channel sends data off the phone, which the privacy
policy rules out today, so the entry now states that decision and the
standalone alternative without speculating about notification bridging.

Refs #27
2026-10-22 11:05:53 +02:00
makiolaj 94f09e9e5a feat(alarms): choose what the volume buttons do while ringing
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
2026-10-22 11:04:39 +02:00
makiolaj cb5cd5123b docs(roadmap): keep the Wear OS note to what holds
Ringing notifications are foreground-service notifications, so making
them non-ongoing wouldn't bridge them; CompanionDeviceManager carries no
data; and watches pair through their makers' apps. Say only what holds
and note that a watch build has no F-Droid channel.

Refs #27
2026-10-22 11:03:44 +02:00
makiolaj 0213af1260 docs(roadmap): correct what a watch gets today
Ring and timer notifications are ongoing, which Wear OS doesn't bridge,
and bridging needs Google's Wear OS app anyway. Quote the privacy policy
exactly, drop the unchecked "only channel" claim, and list bridging the
ring as an option of its own.

Refs #27
2026-10-22 11:01:02 +02:00
makiolaj 95bb2d14ec docs(roadmap): record why a Wear OS companion is blocked on Play Services
A companion needs Google's Wearable Data Layer, which is a proprietary
service the privacy policy and the F-Droid build rule out. The roadmap
entry now records that constraint, what the bridged notifications
already give a watch, and the options if it is picked up.

Refs #27
2026-10-22 10:59:35 +02:00
makiolaj 712b606779 fix(alarms): keep the upcoming notice off watches, and check past the grace
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
2026-10-22 08:21:33 +02:00
makiolaj 5ad9bd02d6 fix(alarms): spend a pending skip only after its fire grace
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
2026-10-22 08:13:31 +02:00
makiolaj fdb725e64c fix(alarms): spend a skip at its occurrence again
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
2026-10-22 08:07:52 +02:00
makiolaj a88e696d52 fix(alarms): spend a skip only after its fire grace
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
2026-10-22 08:00:38 +02:00
makiolaj b4af164826 fix(alarms): spend a dismissed skip on time, and ring a re-armed handled alarm
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
2026-10-22 07:51:57 +02:00
makiolaj f570bdefb7 fix(alarms): keep the upcoming notice off a locked screen below Android 12
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
2026-10-22 07:41:05 +02:00
makiolaj 1253c0fc55 fix(alarms): no fire for an occurrence already accounted for
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
2026-10-21 20:26:50 +02:00
makiolaj dedcbd5dfd fix(alarms): a consumed skip stays handled, and only a swipe hides a notice
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
2026-10-21 20:18:05 +02:00
makiolaj 5b35ef65d4 fix(alarms): keep the upcoming Dismiss away from the fire
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
2026-10-21 20:09:47 +02:00
makiolaj e43339c386 feat(alarms): notify before an alarm rings, with Dismiss
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
2026-10-21 20:01:21 +02:00
makiolaj 6239ca0477 refactor(settings): read a silence-after of an hour or more as Never
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
2026-10-21 19:45:44 +02:00
makiolaj 1689794da6 fix(alarms): re-anchor a ring behind a moved clock, own the timer write
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
2026-10-21 19:38:37 +02:00
makiolaj 9516dc1198 refactor(alarms): derive the alarm-set notice from the engine alone
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
2026-10-21 19:35:29 +02:00
makiolaj 0b7af0d57b fix(alarms): let the backstop always close the ring it was armed for
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
2026-10-21 19:28:09 +02:00
makiolaj 4cb4b6b6eb fix(alarms): keep clock changes and setting writes inside one window
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
2026-10-21 19:20:25 +02:00
makiolaj d2bf4349c5 fix(alarms): never announce an alarm that has since stopped ringing
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
2026-10-21 19:18:19 +02:00
makiolaj fb875ca71c fix(alarms): measure every ring against one window rule
Second round of review fixes for the silence-after setting.

A fire now clears any stale ringingSince before it re-resolves the slot.
Before, the resolver's new one-shot close could switch off, or delete, the
very alarm that was about to ring.

A one-shot alarm displaced by another alarm's fire is switched off, as on
every other close of an undismissed cycle, instead of being armed for the
same time tomorrow.

onAutoSilence, onSilenceAfterChanged and the reboot resume now share one
rule. A ring still inside the window has its backstop re-armed, measured
from when it started, or from now if the clock has moved back behind that
start, so a clock change cannot stretch a ring past one window.

Refs #17
2026-10-21 19:12:18 +02:00
makiolaj fa5ce55503 fix(alarms): settle and show the alarm-set notice where it can't be lost
The repeat-day settle ran on the editor's scope, so leaving mid-settle
needed a flush that a cleared scope could still race. The announcer now
owns the settle on the application scope. Screens show the notice only
while resumed, so a cross-fade never shows it twice and a backgrounded
app does not use it up unseen. The editor shows only notices about its
own alarm, since the sentence names none.

Deleting an alarm, or the AlarmClock chooser dismissing it, takes its
notice back. Freshness is measured on the monotonic clock, one instant
is used for both ends of the countdown, and an identical countdown
keeps the standing notice instead of flickering it.

Refs #19
2026-10-21 19:10:42 +02:00
makiolaj 1c323d5b4c fix(alarms): apply silence-after mid-ring and close missed one-shots
Review fixes for the silence-after setting.

Changing the alarm's silence-after now re-measures a ring in progress at
once, through AlarmEngine.onSilenceAfterChanged: past the new window it
closes as missed, inside it the backstop moves. onAutoSilence also re-arms
instead of closing a ring that is still inside the current window, so a
backstop armed for a shorter window cannot end a ring early.

A cycle the resolver closes as missed (a ring past its window, or a
snooze that never fired) now disables a one-shot alarm, as the engine's
own close does. A short window made that path much easier to reach, and
the alarm was being armed for the same time the next day.

A late snooze is kept for the window or ten minutes, whichever is longer,
so a short window never makes a promised snooze easier to lose than in v1.

A timer that has gone quiet stays quiet when the setting is raised: the
lapsed anchor moves back past the hard stop. The timer's heads-up is now
keyed to the session anchor rather than the window's end, so editing the
setting mid-ring no longer counts as a new session and alerts again.

A negative stored value now reads as the default instead of Never. The
picker offers a stored value that matches no choice, and the wake-lock
timeout is one shared constant.

Refs #17
2026-10-21 19:04:08 +02:00
makiolaj 207475ef37 fix(alarms): keep the alarm-set notice until a screen has shown it
The usual way to set an alarm is pick, OK, back, and back disposed the
editor's snackbar a moment after it appeared. Day taps left within the
one-second settle were never announced at all, and a rotation threw a
showing snackbar away for good.

The announcer is now an app-wide singleton holding the notice as state
until a snackbar has run to the end, so the list picks up what the
editor could not finish showing. An editor closed mid-settle announces
on the way out. Announcing is serialised, so a quick on-then-off cannot
land the "on" read last, and a notice nobody showed within ten seconds
is dropped rather than shown stale. The engine's one-alarm read now
shares upcoming()'s preview helper.

Refs #19
2026-10-21 19:00:56 +02:00