feat(settings): let alarms and timers choose when they silence themselves
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
This commit is contained in:
1 parent
3f431a7f32
commit
412e54db09
33 files changed
+680
-80
No files matched your search
+45
-12
@@ -1050,14 +1050,14 @@ M10 adds no `<uses-permission>`.
|
||||
| Not here | Milestone |
|
||||
|---|---|
|
||||
| Reordering alarms by hand. The list is ordered by time of day (§13) and `alarms` has no `sort_order` column, so this would be a schema change for a preference the time order already answers | not planned for v1 |
|
||||
| A per-alarm auto-silence duration. Every other per-alarm setting became an override picker in M5; this one is still global and still `AlarmRing.AUTO_SILENCE_AFTER` | not planned for v1 |
|
||||
| A per-alarm auto-silence duration. Every other per-alarm setting became an override picker in M5; "Silence after" (#17) is an app-wide setting only, because an override would need a new `alarms` column, a migration and an editor row for a value almost nobody sets per alarm | not planned |
|
||||
| Voice-interaction follow-on flows: `Activity.isVoiceInteraction`, `VoiceInteractor.CompleteVoiceRequest` (publishing a deeplink to the alarm just created) and `PickOptionRequest` (disambiguating by voice). `android.intent.category.VOICE` is therefore not declared: advertising it while answering with a touch dialog would be a promise the app does not keep. Clockula *accepts* a `clockula://` deeplink and publishes none | not planned for v1 |
|
||||
| A "most closely matched" time search for `DISMISS_ALARM`. `ALARM_SEARCH_MODE_TIME` matches **exactly**: dismissing an alarm the caller did not name is a missed alarm, and there is no distance at which that becomes a good trade | not planned for v1 |
|
||||
| A toast or any other confirmation on a `SKIP_UI` write. `SKIP_UI` means "bypass any intermediate UI", a voice assistant speaks its own confirmation, and the status-bar alarm icon `setAlarmClock` lights is the platform's own receipt (§17) | not planned for v1 |
|
||||
| Surfacing `delete_after_use` in either editor. It is a property the contract sets, not a setting; an alarm the user edits keeps it, and that consequence is documented (§17) rather than papered over with a checkbox | not planned for v1 |
|
||||
| Carrying `EXTRA_MESSAGE` into the timer setup panel when no length was given. There is no row to carry it on and the panel has no label field | not planned for v1 |
|
||||
| Verifying a ringtone URI at the intent boundary. M5 locked "an unreadable sound is reported, never rewritten"; an unmounted card comes back, and the boundary opens no `ContentResolver` | not planned for v1 |
|
||||
| A user-configurable auto-silence duration, an unlimited-snooze option, per-alarm auto-silence | not planned for v1 |
|
||||
| An unlimited-snooze option, per-alarm auto-silence | not planned for v1 |
|
||||
| Reordering timers by hand. `sort_order` and `TimerRepository.reorder` exist and stay caller-less: the list is in storage order, M5 gave the same answer for alarms, and a drag surface would also mean abandoning the `LazyColumn` | not planned for v1 |
|
||||
| Per-timer vibrate, volume-ramp or dismiss-challenge overrides. `timers` has exactly one nullable settings column, its ringtone; four more would be a schema change for settings the roadmap does not ask for | not planned for v1 |
|
||||
| A "stop all" for several expired timers, and undo after a timer delete. Each expired timer is acknowledged on its own, and the delete confirmation asks before rather than after | not planned for v1 |
|
||||
@@ -1142,7 +1142,7 @@ repair, resume and reschedule one by one, so its whole pass is inside the lock.
|
||||
|
||||
1. **Disabled** ⇒ clear the snooze, the ring and the skip watermark; keep
|
||||
`handled_occurrence`, because re-enabling must not un-suppress a dismissal.
|
||||
2. **Ringing** within `AUTO_SILENCE_AFTER` ⇒ `RESUMED_RING`, nothing scheduled:
|
||||
2. **Ringing** within the "Silence after" window ⇒ `RESUMED_RING`, nothing scheduled:
|
||||
it is not an alarm to come, it is one that is happening. A clock moved
|
||||
*backwards* under a ringing alarm reads as a negative age, which is inside the
|
||||
window, so it keeps ringing. Beyond the window it is auto-dismissed as
|
||||
@@ -1157,8 +1157,32 @@ repair, resume and reschedule one by one, so its whole pass is inside the lock.
|
||||
### The ring cycle
|
||||
|
||||
A cycle opens when the alarm fires and closes on dismiss, on the snooze limit
|
||||
being reached, or on auto-silence after `AUTO_SILENCE_AFTER` = **10 minutes**
|
||||
(AOSP DeskClock's default; not user-configurable in v1). Snoozing keeps it open.
|
||||
being reached, or on auto-silence after the **"Silence after"** window. That is
|
||||
a setting since #17 (Settings › Alarms: 1, 5, 10, 15, 20 or 25 minutes, or
|
||||
Never), **10 minutes** by default, AOSP DeskClock's default and v1's fixed
|
||||
value, so an upgrade changes nothing. Snoozing keeps it open.
|
||||
|
||||
The window is read from `ClockDefaults.alarmSilenceAfter` everywhere a ring is
|
||||
measured: the resolver's ringing and late-snooze checks, the backstop `onFire`
|
||||
arms, how late a snooze fire may arrive, and whether a ring interrupted by a
|
||||
reboot is resumed. `AlarmResolver.resolve` takes it without a default, so no
|
||||
caller can measure against a window the user did not choose.
|
||||
|
||||
**"Never" is an hour.** The backstop is also what stops a phone left at home
|
||||
from ringing until its battery is flat, what bounds the ring services' wake
|
||||
locks, and what keeps a reboot hours later from resuming a ring nobody was
|
||||
there for. So "Never" maps to `SilenceAfter.HARD_STOP` (60 minutes), longer
|
||||
than any finite choice, and the settings hint says so in words. Stored values
|
||||
are clamped to 0..60 on read, on write and from a backup, so no value can make
|
||||
a ring outlast it.
|
||||
|
||||
A ring started under one window keeps the backstop it was armed with. When the
|
||||
window is shortened mid-ring, the next re-resolution closes the ring once it is
|
||||
past the new window, and `persist` then stops the ring service and cancels the
|
||||
backstop with it: storage says nothing rings, so nothing may. The same applies
|
||||
to any ring the resolver closes (a clock jumped forward, a ringing alarm
|
||||
switched off), which earlier only cleared `ringingSince` and left the sound to
|
||||
the backstop.
|
||||
|
||||
A **non-repeating alarm is disabled when its cycle closes, never when it opens**.
|
||||
Disabling it at the start would clear its own snooze and the alarm would vanish
|
||||
@@ -1284,7 +1308,7 @@ timer too. What the alarm **keeps to itself** is the full-screen intent, the
|
||||
ring activity, the dismiss challenge and the snooze; what the timer
|
||||
parameterises is its own ringtone, a ramp of **zero seconds** (a timer is set by
|
||||
someone awake, and a gentle start is just a quiet start) and its own
|
||||
auto-silence constant. §14 has the rest, including which of the two wins the
|
||||
"Silence after" setting. §14 has the rest, including which of the two wins the
|
||||
one audio stream.
|
||||
|
||||
---
|
||||
@@ -1433,7 +1457,7 @@ whichever comes first, for every challenge kind, asserted as a parameterised
|
||||
invariant over `DismissChallenge`, not as three happy paths.
|
||||
|
||||
Three more guarantees hold alongside it. Snooze is never gated: the gate protects
|
||||
dismissal only. Auto-silence still ends the ring at ten minutes regardless of the
|
||||
dismissal only. Auto-silence still ends the ring at its "Silence after" window regardless of the
|
||||
gate, because that is the engine's, and the screen only follows the session to
|
||||
`Finished`. And a wrong answer carries no lockout and no penalty timer; it
|
||||
replaces the problem and that is all.
|
||||
@@ -1693,6 +1717,12 @@ the engine's own mutex, so it cannot race the fire it is asking about, and it is
|
||||
deliberately **conditional**: dismissing unconditionally would also drop a
|
||||
merely *snoozed* alarm's pending snooze, which is wrong for a label edit.
|
||||
|
||||
Since #17 the engine's `persist` also stops the ring service and cancels the
|
||||
backstop whenever the resolver closes a ring (§11), so the disabled branch no
|
||||
longer leaves a ring sounding on its own. The dismiss-first rule stays: it is
|
||||
what closes the cycle *as a dismissal* (a one-shot alarm disabled, the snooze
|
||||
notification cleared), and a deleted row never reaches the resolver at all.
|
||||
|
||||
### Immediate apply
|
||||
|
||||
There is no Save button, no dirty state and no discard dialog: `PLAN.md` §11
|
||||
@@ -1877,8 +1907,8 @@ sound.
|
||||
- **A session is one window.** It opens when the audio *actually starts
|
||||
sounding* and closes when the expired set returns to empty. A second timer
|
||||
expiring mid-session inherits the session's remaining window rather than
|
||||
buying a fresh ten minutes. That is the conservative direction, and the same rule as
|
||||
§11's "a reboot does not buy the alarm another ten minutes". The anchor is the
|
||||
buying a fresh window. That is the conservative direction, and the same rule as
|
||||
§11's "a reboot does not buy the alarm a fresh window". The anchor is the
|
||||
instant the audio *opened* rather than the instant of expiry, so a long alarm ring in
|
||||
front of it does not eat the timer's whole window.
|
||||
- **A lapsed window does not silence the next timer.** A window that has run out
|
||||
@@ -1889,9 +1919,12 @@ sound.
|
||||
opens a fresh window with its own heads-up. Inheriting stays the rule for a
|
||||
timer expiring *inside* a live window.
|
||||
|
||||
`TimerRing.AUTO_SILENCE_AFTER` is **ten minutes**: its own constant with the
|
||||
same value as the alarm's, free to diverge, because coupling the two would make
|
||||
one number answer two questions. Unlike an alarm's, a timer's
|
||||
The window is the timer's own **"Silence after"** setting
|
||||
(`ClockDefaults.timerSilenceAfter`, Settings › Timers, #17), ten minutes by
|
||||
default: the same choices as the alarm's and the same one-hour "Never", but a
|
||||
separate value, because coupling the two would make one number answer two
|
||||
questions. `TimerRingPolicy` reads it from the defaults it already takes, so a
|
||||
change applies on the service's next pass. Unlike an alarm's, a timer's
|
||||
**auto-silence stops the noise and leaves the row `EXPIRED`.** An auto-silenced
|
||||
alarm is a *missed* alarm and its cycle closes; an auto-silenced timer has still
|
||||
finished, and the user coming back must be able to see that and add a minute
|
||||
|
||||
+11
-2
@@ -136,6 +136,8 @@ device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
|
||||
"default_timer_ringtone_uri": null,
|
||||
"default_dismiss_challenge": "NONE",
|
||||
"home_zone_id": null,
|
||||
"default_alarm_silence_after_minutes": 10,
|
||||
"default_timer_silence_after_minutes": 10,
|
||||
"timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000]
|
||||
}
|
||||
```
|
||||
@@ -143,8 +145,15 @@ device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
|
||||
Field names mirror `ClockPrefs`'/`SettingsPrefs`' own on-disk keys. Older
|
||||
files may also carry `default_timer_duration_millis`, from a default timer
|
||||
length setting that has since been removed; it is ignored on import like any
|
||||
other unknown field. Every
|
||||
numeric/duration value is clamped on *apply* using the same ranges and encoding
|
||||
other unknown field.
|
||||
|
||||
`default_alarm_silence_after_minutes` and `default_timer_silence_after_minutes`
|
||||
are how long a ringing alarm or a finished timer keeps going before it stops on
|
||||
its own: whole minutes, with `0` meaning "Never" (which still stops after an
|
||||
hour). A file without them, from before the setting existed, imports as `10`,
|
||||
the value it was made with. Out-of-range values clamp to 0..60.
|
||||
|
||||
Every numeric/duration value is clamped on *apply* using the same ranges and encoding
|
||||
as the existing preference setters, never on parse.
|
||||
|
||||
## Ringtone URIs
|
||||
|
||||
Reference in new issue
Block a user