feat(timers): give timers their own vibration and gradual volume
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
This commit is contained in:
1 parent
3f431a7f32
commit
fbc3a192bf
21 files changed
+375
-38
No files matched your search
+24
-10
@@ -595,7 +595,7 @@ Four slices:
|
||||
| Slice | Keys | Owner |
|
||||
|---|---|---|
|
||||
| Appearance (M1) | `theme_mode`, `dynamic_color` | `AppearancePrefs` in `core-prefs`, the family's shared key names |
|
||||
| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_dismiss_challenge`, `home_zone_id` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow<ClockDefaults>` |
|
||||
| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_timer_vibrate`, `default_timer_volume_ramp_seconds` (#22), `default_dismiss_challenge`, `home_zone_id` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow<ClockDefaults>` |
|
||||
| Stopwatch run record (M2) | `stopwatch_state`, `stopwatch_started_elapsed_millis`, `stopwatch_accumulated_millis`, `stopwatch_last_lap_cumulative_millis` (M7 is the first caller of the last, for the in-progress lap's split) | `StopwatchPrefs`, behind `StopwatchStateStore` |
|
||||
| System facts (M3) | `last_boot_id` | `SystemPrefs`, behind `BootStateStore`, the persisted half of the reboot gate |
|
||||
| Timers (M6) | `timer_presets`, `timer_ring_sounding_since_millis` | `TimerPrefs`: the presets surfaced as `SettingsPrefs.timerPresets`, the ring session's anchor behind `TimerRingStateStore` |
|
||||
@@ -1062,7 +1062,7 @@ M10 adds no `<uses-permission>`.
|
||||
| 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 |
|
||||
| A presets management screen (add/remove the quick-start durations themselves). M10's settings screen edits `default_timer_ringtone_uri`, but the preset list is still only editable from the two-gesture surface on the setup panel | not planned for v1 |
|
||||
| A timer that ramps its volume, a dismiss challenge or a snooze for a timer | not planned for v1 |
|
||||
| A dismiss challenge or a snooze for a timer. (A timer that ramps its volume landed with #22, as an app-wide timer setting.) | not planned for v1 |
|
||||
| Surfacing `TimerSnapshot.anchorIsStale` in the UI. The boot repair makes it vanishingly rare and the value is already honest; a row does not say "estimated" in v1 | not planned for v1 |
|
||||
| Lap export, sharing, deletion or editing, and any "previous run" history. A run's laps belong to the run that produced them; keeping old runs is a different feature with its own table | not planned for v1 |
|
||||
| A Lap button on the live pill. The pill has two controls by design, and a third would rewrite a contract M4 asserted | not planned for v1 |
|
||||
@@ -1282,8 +1282,8 @@ loop, the ramp stepper, the looped waveform, `AudioSourcePolicy`,
|
||||
and are used verbatim by both, so "silent" still means "vibration only" for a
|
||||
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
|
||||
parameterises is its own ringtone, its own vibration and ramp settings (#22;
|
||||
the ramp is **zero seconds** unless the user asks for one) and its own
|
||||
auto-silence constant. §14 has the rest, including which of the two wins the
|
||||
one audio stream.
|
||||
|
||||
@@ -1898,11 +1898,24 @@ finished, and the user coming back must be able to see that and add a minute
|
||||
to it. Auto-silence is decided **before** suppression, so an alarm ringing over
|
||||
a window that has run out cannot hand it a fresh one.
|
||||
|
||||
`TimerRing.RAMP_SECONDS` is **0**. The volume ramp exists so an alarm does not
|
||||
A timer's ramp is `ClockDefaults.timerVolumeRampSeconds`, **0** unless the
|
||||
user picks one in Settings > Timers (#22; it was the constant
|
||||
`TimerRing.RAMP_SECONDS` before). The volume ramp exists so an alarm does not
|
||||
jolt a sleeping person awake; a timer is set by someone awake who wants to know
|
||||
*now*, and for them a gentle start is a quiet start. `VolumeRamp.levelAt(_, 0s)`
|
||||
already returns `1f`, so "parameterised" means one different argument to the
|
||||
same player.
|
||||
*now*, and for most of them a gentle start is a quiet start, so the default
|
||||
stays flat. `VolumeRamp.levelAt(_, 0s)` already returns `1f`, so
|
||||
"parameterised" means one different argument to the same player.
|
||||
|
||||
Vibration is `ClockDefaults.timerVibrate`, never the alarms' `vibrate`: an
|
||||
alarm that should buzz the bed and a kitchen timer that should only beep are
|
||||
different questions. A fresh install has it **off**. An install from before
|
||||
the setting existed had its timers vibrate exactly when the alarm default did,
|
||||
so `TimerVibrateMigration` (a DataStore migration, run before the first read)
|
||||
writes that value once rather than silently changing what its timers do; it
|
||||
tells a fresh install by an empty store, since anything that has run has
|
||||
written its boot id. Silent with vibration off is still not silence:
|
||||
`AudioSourcePolicy` gives Silent no sources, and the player's `onSilenced`
|
||||
starts the vibrator regardless of the setting.
|
||||
|
||||
### The notification: one channel, one id, alert once
|
||||
|
||||
@@ -2060,8 +2073,9 @@ ever read it, so it was removed. A backup file that still carries the key
|
||||
imports with it ignored.)
|
||||
|
||||
A timer has exactly **one** per-setting override, its ringtone, because `timers`
|
||||
has exactly one nullable settings column. Vibrate and the (zero) ramp come from
|
||||
`ClockDefaults`. The picker is M5's, moved to `ui/common/`, so the row inherits
|
||||
has exactly one nullable settings column. Vibrate and the ramp come from
|
||||
`ClockDefaults`' timer-only `timerVibrate` and `timerVolumeRampSeconds` (#22),
|
||||
not the alarm defaults. The picker is M5's, moved to `ui/common/`, so the row inherits
|
||||
all of its behaviour for free: "App default" naming what it resolves to, Silent
|
||||
as an explicit sentinel that forces vibration, a SAF-picked file that is still
|
||||
stored when its grant cannot be made persistent, and an unreadable sound
|
||||
|
||||
Reference in new issue
Block a user