fix(alarm): scope native stop to the ringing id and intercept system back
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m43s

Two exit-path holes found by adversarial review before the next build:

PluriWaveAlarmService.stopAlarm() never compared the requested id to
activeAlarmId, so any stop request for a DIFFERENT alarm tore down
whichever ring was active: with two alarms firing close together, the
second one's routine hide-notification call (via dismissAlarmNotification
-> ACTION_STOP) killed the first alarm mid-ring and prematurely restored
the device volume override. A mismatched id now only cancels that id's
notification and returns; null keeps full-teardown semantics for
internal/onDestroy callers.

The ringing screen never intercepted the system back gesture: a plain
route pop ran only dispose(), leaving the shared radio player ringing
with no alarm UI left anywhere to stop it. Back now routes through
PopScope into the same _detener() flow as the Stop button, guarded by a
single-exit flag so a back-press racing a button tap cannot run the
teardown twice and pop the route underneath.

Also resets the shared handler gain to 1.0 on ring exit: the fade-in
mutates the radio player's persistent volume, and exiting mid-ramp used
to leave every later radio play at the partial ramp level.
This commit is contained in:
2026-07-12 00:04:53 +02:00
parent e84a41acd4
commit 86225cbc68
3 changed files with 105 additions and 0 deletions
@@ -360,6 +360,24 @@ class PluriWaveAlarmService : Service() {
private fun stopAlarm(alarmId: String?) {
Log.d(TAG, "alarm.service stop id=$alarmId active=$activeAlarmId")
// Scope the teardown to the alarm that is actually ringing: a stop
// request for a DIFFERENT id (e.g. a second alarm firing while this
// one rings — Dart hides the newcomer's notification, which routes
// through ACTION_STOP with the newcomer's id) must not kill the
// active ring, release its wake lock, or prematurely restore the
// device volume. Only the id-specific notification cancel below is
// honored for the mismatched id. A null alarmId (internal callers,
// onDestroy) keeps full-teardown semantics.
if (alarmId != null && activeAlarmId != null && alarmId != activeAlarmId) {
Log.d(
TAG,
"alarm.service stop ignored for id=$alarmId (active=$activeAlarmId)"
)
NotificationManagerCompat.from(this).cancel(
PluriWaveAlarmReceiver.fireNotificationIdForAlarm(alarmId)
)
return
}
cancelStationFallback()
cancelFadeIn()
try {