feat(alarms): swipe to delete, with Undo
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
This commit is contained in:
1 parent
3f431a7f32
commit
2feed2ee35
34 files changed
+1074
-114
No files matched your search
+63
-8
@@ -1060,7 +1060,7 @@ M10 adds no `<uses-permission>`.
|
||||
| A user-configurable auto-silence duration, 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 |
|
||||
| A "stop all" for several expired timers. Each expired timer is acknowledged on its own | 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 |
|
||||
| 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 |
|
||||
@@ -1688,7 +1688,8 @@ the backstop disarmed by its own guard**. Both ViewModels therefore call
|
||||
if (engine.ringSession()?.alarm?.id == alarmId) engine.dismiss(alarmId)
|
||||
```
|
||||
|
||||
before `setEnabled(id, false)` and before `delete(id)`. It reads storage under
|
||||
before `setEnabled(id, false)`. Deleting goes further and is an engine verb of
|
||||
its own (`AlarmEngine.delete`, below), which stops the ring itself. The check reads storage under
|
||||
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.
|
||||
@@ -1712,10 +1713,52 @@ a **repeating** alarm. The resolver answers a one-shot-with-skip by disabling th
|
||||
alarm, and a switch labelled "skip" that silently switched the alarm off would
|
||||
be a lie; the user who wants that has the enable switch.
|
||||
|
||||
Delete lives in the editor behind a confirmation that names the alarm, and there
|
||||
is deliberately no undo chip: the delete happens on the editor, so an undo chip
|
||||
would have to live on the list, and an accidentally deleted alarm is discovered
|
||||
at 07:00 rather than now; the cheap moment to ask is before.
|
||||
### Delete, and the Undo
|
||||
|
||||
An alarm is deleted by swiping its card towards the end edge, by the card's
|
||||
**Delete** custom accessibility action (the same verb for TalkBack and switch
|
||||
access, where there is no swipe), or from the editor's top bar. None of them
|
||||
asks first. Each is followed by a snackbar on the list ("07:00 alarm deleted",
|
||||
**Undo**), as in Google Clock. v1 asked before instead, on the argument that a
|
||||
mistaken delete is found at 07:00 rather than now. But a dialog catches no more
|
||||
than the snackbar does: both only catch the slip noticed in the moment, and a
|
||||
confirmation the user taps through out of habit catches less. The Undo does it
|
||||
without taxing every delete the user meant, so the editor's dialog went too
|
||||
rather than leaving two delete paths with different safety nets.
|
||||
|
||||
`AlarmEngine.delete(id)` does the work under the engine's lock: it stops a
|
||||
ringing alarm's service and auto-silence backstop (the global slots of §11),
|
||||
clears a snoozed alarm's notification, deletes the row (the ring state
|
||||
cascades) and re-resolves the slot. It returns a `DeletedAlarm`, the row and
|
||||
its ring state as `AlarmDeletion.snapshot` decided, and `AlarmEngine.restore`
|
||||
puts that back **under the same id** (`alarms.id` is `AUTOINCREMENT`, so no new
|
||||
alarm can have taken it) and re-resolves, which is what re-registers it with
|
||||
AlarmManager. The snapshot keeps every setting and the ring state as it was, so a
|
||||
snoozed alarm comes back snoozed, notification included, and the handled-occurrence
|
||||
watermark still stops a just-closed occurrence ringing twice. A **ringing**
|
||||
alarm is the exception: deleting it was a dismissal in all but name, so it comes
|
||||
back as a dismissal would have left it (cycle closed, a one-shot switched off)
|
||||
and never rings again, and a ringing transient alarm, which that dismissal would
|
||||
have deleted anyway, offers no Undo at all. A snooze that lapsed while the alarm
|
||||
was gone is closed by the resolver on restore, and gets no notification. An id
|
||||
that a backup restore claimed in the meantime makes the restore write nothing.
|
||||
|
||||
The pending deletion lives in `AlarmUndo`, a singleton, not in either
|
||||
ViewModel. The editor is popped the moment its alarm is gone, so its ViewModel
|
||||
is cleared before any snackbar could be answered; the list underneath reads the
|
||||
deletion from the singleton and shows the snackbar. It is cleared only when the
|
||||
snackbar is answered or times out, or a newer deletion replaces it, so a
|
||||
rotation or a trip to another tab re-shows the snackbar rather than losing the
|
||||
Undo. It is memory only: an Undo that outlived process death would be offered
|
||||
with no snackbar anyone remembers.
|
||||
|
||||
The swipe is hand-rolled (`ui/common/SwipeToDelete`) rather than M3's
|
||||
`SwipeToDismissBox`, because the tabs are pages of a `HorizontalPager` and the
|
||||
box claims a horizontal drag in both directions, even one it has no anchor for.
|
||||
Only the start-to-end drag is claimed; the end-to-start one is left unconsumed,
|
||||
so a swipe across a card still changes tab. Start-to-end is also the pager's
|
||||
"previous tab" direction, which from Alarms, the first tab, goes nowhere, so the
|
||||
two gestures never want the same drag.
|
||||
|
||||
---
|
||||
|
||||
@@ -1751,6 +1794,17 @@ already met once, and the cheapest way not to have it again is to make the wrong
|
||||
call impossible to write. **Configuration** edits (`rename`, `setDuration`,
|
||||
`setRingtoneUri`) stay on `TimerRepository` and the editor calls them directly.
|
||||
|
||||
A card's corner delete asks nothing first, and the list's snackbar offers an
|
||||
**Undo**. `TimerEngine.delete` returns the row as `TimerDeletion.snapshot`
|
||||
decided, and `TimerEngine.restore` puts it back under its own id and place and
|
||||
resyncs. A running timer comes back with its anchors, so it has kept counting
|
||||
while it was gone, and one whose end passed in the meantime rings, as it would
|
||||
have had it never been deleted. A timer that had already finished comes back
|
||||
reset: deleting it silenced it, which is a dismissal, so it does not ring a
|
||||
second time (a finished transient timer offers no Undo). The pending row is
|
||||
`TimersViewModel` state, since both the delete and the snackbar are on the
|
||||
list; the editor's delete keeps its confirmation.
|
||||
|
||||
`TimerRepository` deliberately has **no generic `edit(id, transform)`** the way
|
||||
`AlarmRepository` does: a timer row carries three derived anchors whose
|
||||
consistency is the whole of §5, and a caller handed a `(Timer) -> Timer` can
|
||||
@@ -2462,8 +2516,9 @@ an id the list does not hold, and always a permutation of its input; a move that
|
||||
changes nothing writes nothing.
|
||||
|
||||
Remove is a long-press plus a confirmation that names the city, with
|
||||
deliberately **no undo chip**. That is the same trade M5 and M6 made: the cheap
|
||||
moment to ask is before. The pending id is `rememberSaveable`, so a rotation
|
||||
deliberately **no undo chip**. That was the trade M5 and M6 made too, and the
|
||||
alarm and timer lists have since moved to an Undo (§13, §14); removing a city
|
||||
still asks first. The pending id is `rememberSaveable`, so a rotation
|
||||
mid-confirmation does not lose the question.
|
||||
|
||||
A row whose zone the device's tzdata no longer knows shows **no time, no offset
|
||||
|
||||
+3
-2
@@ -247,7 +247,8 @@ line ("in 9h 12m") on each row. Built on `GroupedSurface`/`GroupedRow`.
|
||||
|
||||
Knowingly open: no hand reordering of alarms (the time order answers it, and
|
||||
`alarms` has no `sort_order` column), no multi-select or bulk delete, no undo
|
||||
chip after a delete (traded for a confirmation that names the alarm), no
|
||||
chip after a delete (traded for a confirmation that names the alarm; since
|
||||
replaced by swipe to delete with Undo, #20), no
|
||||
repeat presets, and no per-alarm auto-silence (still M10 at the earliest).
|
||||
Editing the app-wide defaults every override row offers to follow is M10's
|
||||
settings screen. The seven new instrumentation tests compile in the gate but
|
||||
@@ -316,7 +317,7 @@ Elapsed-realtime anchored.
|
||||
per-timer vibrate, volume-ramp or dismiss-challenge overrides (`timers` has
|
||||
one nullable settings column and four more would be a schema change), no
|
||||
"stop all" when several timers are expired, no undo after a delete (traded for
|
||||
a confirmation, as with alarms), no ramp on a timer's ring, no presets
|
||||
a confirmation, as with alarms; added since, #20), no ramp on a timer's ring, no presets
|
||||
management screen and no editing of `default_timer_duration_millis` or
|
||||
`default_timer_ringtone_uri` (M10's settings screen). `TimerSnapshot.anchorIsStale`
|
||||
is still not surfaced in the UI. Of `AlarmClock`'s intent contract only the
|
||||
|
||||
Reference in new issue
Block a user