# Proposal: Alarm System Overhaul — Fail-Safe Stop/Dismiss ## Intent A real alarm rang ~15 min and could not be stopped by any means; the user uninstalled. Two confirmed causes: (1) native `stopAlarm` silently no-ops on `alarmId != activeAlarmId` while the Dart screen dismisses regardless ("dismiss-by-design" masks the failure); (2) disabling/editing a ringing alarm never stops audio. No bounded auto-silence and no durable firing record exist — an unbounded ring is the literal incident. Make the alarm system fail-safe per the acceptance bar (always rings; every action does exactly what it says; stop/snooze/disable/edit always silence a live ring), grounded in the Alarm Platform Contract and verifiable WITHOUT an Android build. ## Scope ### In Scope - **P0 — Stop/dismiss correctness (incident class):** - Fail-safe native stop: the ringing UI and notification Stop action use id-agnostic `stopActive` semantics — stop MUST NEVER silently no-op. Concurrent-alarm case uses verify-and-report; ambiguity fails toward silence. - Any Dart mutation of the ringing alarm (disable/edit/save/delete/snooze) stops audio first — shared guard in `EstadoAlarmas`. - Bounded auto-silence (fixed 10 min this slice) via AlarmManager-scheduled `FIRED→MISSED` transition; repeating alarms rearm next occurrence; one-shots disable. - Atomic stop: `stopForeground(REMOVE)` + `stopSelf()` + wakelock release in one path. - Durable "firing since T" record + `onStartCommand` re-validation; `START_NOT_STICKY` (no blind resurrection); stale record past timeout → clean up as missed. - Dart verification feedback: stop/snooze channel calls return success/failure; dismiss-by-design stays, but failures raise a persistent retryable "Force stop" affordance. - **P1 — Hardening:** `canUseFullScreenIntent()` check + heads-up fallback; exact-alarm-denial and `POST_NOTIFICATIONS`-denial in-app warnings. ### Out of Scope - iOS; Android Auto local-music; radio-playback behavior. - **P2 (deferred):** dedicated ringing Activity (vs MainActivity routing); formal state-machine class; belt-and-suspenders `audio_service` teardown on stop (native already owns ring audio); user-configurable silence duration. - Executing the stale `alarm-clock-module` / `app-quality-and-native-alarms` tasks (see Dependencies). ## Capabilities ### New Capabilities - `alarm-stop-safety`: Dart-side fail-safe orchestration — any mutation targeting the ringing alarm guarantees audio stops; stop/snooze expose verifiable success/failure with a retryable force-stop path. ### Modified Capabilities - `native-alarms`: stop never silently no-ops (fail-safe id semantics); atomic `stopForeground`+`stopSelf`+wakelock coupling; bounded auto-silence → `MISSED` with next-occurrence rearm; durable firing record + `onStartCommand` re-validation; `START_NOT_STICKY`; FSI/permission fallbacks. ## Approach Evolve the current architecture — do NOT rewrite. The native service already owns audio and notification actions already use `PendingIntent.getService`. Build on that: add an id-agnostic stop action, a durable firing record, an AlarmManager-armed missed transition, and a coupled stop method. Push all testable logic to Dart (mutation-while-ringing guard, stop-result handling, auto-silence timing/state model) with new fake failure switches; keep Kotlin changes trivially static-grep-verifiable. No new plugin dependencies. ## Affected Areas | Area | Impact | Description | |------|--------|-------------| | `PluriWaveAlarmService.kt` | Modified | Fail-safe `stopActive`, atomic stop, durable record, `onStartCommand` revalidate, auto-silence, `START_NOT_STICKY` | | `AlarmScheduler.kt` | Modified | Arm `FIRED→MISSED` transition; missed cleanup + rearm | | `MainActivity.kt` | Modified | Stop channel returns result; FSI/permission checks | | `estado/estado_alarmas.dart` | Modified | Mutation-while-ringing stop guard; stop/snooze result handling | | `servicios/servicio_alarmas_android.dart` | Modified | Channel methods return success/failure | | `pantallas/pantalla_alarma_sonando.dart` | Modified | Retryable force-stop affordance on failure | | `test/` (Dart) + fakes | New | Failure switches for stop/dismiss; mutation-while-ringing; auto-silence timing | ## Risks | Risk | Likelihood | Mitigation | |------|------------|------------| | Native changes unverifiable without build env | High | Keep Kotlin trivially readable; push logic + tests to Dart; on-device QA checklist | | Fail-safe stop silences the wrong concurrent alarm | Low | Verify-and-report; fail toward silence — stuck-ring cost >> wrong-stop cost | | Auto-silence fires early on a legitimately long ring | Low | 10-min bound matches wakelock cap; configurability deferred to P2 | | P0+P1 exceeds 800-line budget | Med | tasks phase slices P0 first, P1 as follow-up | ## Rollback Plan Single feature branch to `main`. Revert the branch/PR; native and Dart changes are additive to existing stop paths, so reverting restores current behavior. Durable firing record is a new pref key — orphaned entries are self-cleaning (age check), so no migration rollback needed. ## Dependencies - **Stale artifact reconciliation (no work done here):** `alarm-clock-module` (stuck `tasks-ready`) and `app-quality-and-native-alarms` (`apply-complete`, never verified) are SUPERSEDED by this change — this proposal re-specifies the alarm-reliability behavior authoritatively. Recommend archiving both as superseded during this change's archive phase; do NOT re-verify or execute their tasks. ## Success Criteria - [ ] Stop/snooze/disable/edit/delete of a ringing alarm ALWAYS silences audio (Dart tests with failure fakes). - [ ] Native `stopAlarm` has no branch that silently no-ops a live ring. - [ ] A ring cannot exceed the bounded auto-silence window; missed transition + rearm covered. - [ ] Durable firing record + `onStartCommand` re-validation prevent resurrection; `START_NOT_STICKY` set. - [ ] Stop/snooze failures are visible and retryable in-app (force-stop). - [ ] P1 FSI/permission fallbacks present and static-verifiable.