# Proposal: Native Alarm Ring ## Intent The alarm ring has failed 5 consecutive on-device iterations under the media-stream/handoff design (Dart runs a ramp, hands off to native, overrides `STREAM_MUSIC`, restores on dismiss). Root cause: split ring-audio ownership between Dart and native fighting the alarm FGS. This change rebuilds the ring native-only to AOSP DeskClock's exact `AsyncRingtonePlayer` pattern — the pattern that qualifies the app as a legitimate Play Store alarm clock. The current `PluriWaveAlarmService` is already ~80% there; the rebuild mostly DELETES accidental complexity (~500 lines). ## Scope ### In Scope - `PluriWaveAlarmService` becomes the SOLE ring-audio owner for the entire ring: - Replace linear-amplitude fade with DeskClock exponential dB curve @50ms (`gainDb = f*40-40`, `curve = 10^(gainDb/20)`), anchored at ring start. - Per-alarm `volume` becomes the curve ceiling scalar `s` (`setVolume(s*curve)`); NEVER `setStreamVolume` on any stream. - Manual `AUDIOFOCUS_GAIN_TRANSIENT` request on `STREAM_ALARM`; abandon on stop. - Silence the fire notification channel (currently plays `DEFAULT_ALARM_ALERT_URI` on top of the player) → new channel id. - Kill the handoff: delete `confirmFlutterAudio` / `flutterOwnsRing` and the entire media-stream override surface (Dart + MethodChannel + Kotlin override/restore). - Ring screen → pure UI: keep buttons → `EstadoAlarmas.finalizarEjecucion/posponerAlarma`, PopScope back=Stop, navigation/SnackBar semantics. - Delete dead code + dead tests; rewrite ring-screen tests as pure-UI; KEEP the "EstadoRadio never touches override channel" regression test as decoupling proof. - Keep MediaPlayer for playback (already pop-free, already handles stations). ExoPlayer explicitly rejected now (unresolved start-pop #2752). ### Out of Scope (→ follow-up change `oem-reliability-guidance`) - In-app permission-status surface, MIUI/OEM autostart guidance + deep link, receiver-level `PARTIAL_WAKE_LOCK`. ## Do-Not-Touch (hard constraint from exploration) Scheduling stack (AlarmScheduler, boot, ServicioAlarmas/ProgramacionAlarmas, EstadoAlarmas CRUD/snooze/skip/vacations), receiver ACTION_FIRE ordering, wake-lock/FGS-before-audio ordering, snooze 5-entry-point truth-ownership, pre-notice + snooze-countdown chains, radio/EQ/sleep-timer/recording/BT. Only their incidental use by the ring is removed. ## Capabilities ### New Capabilities None. ### Modified Capabilities - `native-alarms`: ring-audio requirements change — single native owner for the full ring; exponential dB fade; per-alarm volume as player-scalar ceiling relative to `STREAM_ALARM`; manual transient alarm focus; silent notification channel; handoff + media-override requirements REMOVED; dismiss restores nothing by construction. ## Approach Converge the existing native service onto DeskClock's `AsyncRingtonePlayer` mechanism and delete the Dart/channel/Kotlin override layer. Native plays until stop/snooze; Dart only reflects state and drives stop/snooze through existing EstadoAlarmas methods. Intent-extra wiring (incl. `EXTRA_FADE_IN_SECONDS`) is already verified end to end. ## Affected Areas | Area | Impact | Description | |------|--------|-------------| | `PluriWaveAlarmService.kt` | Modified | dB curve, volume ceiling, manual focus, silent v3 channel, drop `flutterOwnsRing` | | `MainActivity.kt` | Removed | override/restore methods + companion state + channel handlers | | `app.dart` / `pantalla_alarma_sonando.dart` | Removed/Modified | delete ring-audio orchestration; screen → pure UI | | `PuertoAlarmasAndroid` / `servicio_alarmas_android.dart` | Removed | drop confirmarAudioFlutter / forzar / restaurar surface | | tests | Removed/Rewritten | ramp/handoff/override groups deleted; ring-screen tests → pure-UI; keep decoupling test | ## Risks | Risk | Likelihood | Mitigation | |------|------------|------------| | Silencing channel needs new id (Android locks channel sound at creation; editing `_v2` is a no-op) | High | New `pluriwave_alarm_fire_v3` (`setSound(null)`, keep IMPORTANCE_HIGH for FSI) + `channels_migrated_v3` guard deleting `_v2` — reuse proven `_v2` migration pattern | | Kotlin not agent-compilable | High | On-device QA is the gate: audible curve, volume ceiling, no pop, dismiss/snooze/back, screen-off delivery | | Ring screen visual regression | Low | Behavior unchanged; keep assertions, rewrite only setup | ## Migration Notes - Channel id bump `_v2 → _v3` resets user-visible settings for the ringing channel ONLY (pre-notice channel untouched); note in release notes. Same guarded one-time deletion already shipped for `_v2`. Vibration handling on `_v3` is a design decision. - No data migration: scheduling persistence and alarm model unchanged. ## Rollback Single-change revert restores the prior commit; no schema/persistence change, no data loss. A reverted `_v3` is harmless (service recreates `_v2`). ## Review Workload Estimate Net DELETION (~500 lines removed). Total changed lines exceed the 400-line budget (spread across Kotlin + Dart + tests) → chained/stacked slices recommended. Suggested 3 work-unit split: 1. Kotlin service rebuild (dB curve, manual focus, silent v3 channel, drop `flutterOwnsRing`) — on-device QA gate. 2. Dart deletion + ring-screen rewrite (remove orchestration/override calls; screen → pure UI). 3. Test rewrite (delete dead groups; ring-screen pure-UI tests; keep decoupling regression test). ## Dependencies None blocking. `oem-reliability-guidance` is a downstream follow-up, not a prerequisite. ## Success Criteria - [ ] Native service is the only audio source during ring; notification channel is silent. - [ ] Fade is an audible exponential dB curve capped at the per-alarm volume ceiling; no start pop. - [ ] Dismiss/snooze/back work via EstadoAlarmas; no system volume ever written; nothing to restore. - [ ] Handoff + media-override code and tests fully removed; decoupling regression test passes. - [ ] Screen-off delivery + ring verified on device (POCO X7 Pro) after Autostart enabled.