# Exploration: native-alarm-ring Consolidates three research briefs (2026-07-12): reliable-delivery research (AOSP DeskClock + MIUI/HyperOS), audio-architecture research (DeskClock AsyncRingtonePlayer source study + ExoPlayer/just_audio specifics), and the full code map of the current alarm business (@ `2e64740`). Supersedes the retired media-stream/handoff design after 5 consecutive on-device failures. ## Verdict **The scheduling stack is CORRECT and untouched** (setAlarmClock + receiver + FGS = AOSP DeskClock's exact pattern; verified healthy on the user's device via dumpsys: whitelisted, EXEMPTED bucket, exact Alarm-clock entries). **The ring audio architecture is wrong and gets rebuilt native-only.** ## Target architecture (AOSP DeskClock pattern) ONE player, owned by `PluriWaveAlarmService`, on `USAGE_ALARM`, for the ENTIRE ring. Flutter is pure UI (stop/snooze buttons → existing EstadoAlarmas methods). No handoff, no media-stream override, no Dart ramp, no restore — the design makes "restore" a no-op because system volumes are NEVER written. ### Audio spec (from DeskClock AsyncRingtonePlayer source, verbatim mechanism) - AudioAttributes: `USAGE_ALARM` + `CONTENT_TYPE_SONIFICATION` (music content for station is fine); audio focus requested MANUALLY: `AUDIOFOCUS_GAIN_TRANSIENT` on `STREAM_ALARM` (ExoPlayer auto-focus THROWS for non-media usages — must pass handleAudioFocus=false) - Per-alarm volume = PLAYER scalar `s ∈ (0,1]` via `setVolume(s)`; effective loudness = device STREAM_ALARM knob × s. NEVER call setStreamVolume on ANY stream (neither MUSIC nor ALARM) - Fade (fadeInSegundos > 0): exponential-in-amplitude / linear-in-dB curve, 50ms tick: `fractionDone = clamp(elapsed/fadeMs, 0, 1); gainDb = fractionDone*RANGE - RANGE; curve = 10^(gainDb/20); setVolume(s * curve)` with `RANGE = 40.0` dB (DeskClock constant: -40dB→0dB ≈ 1%→100% amplitude). Anchored at ring start. - No-fade path: `setVolume(s)` BEFORE `start()` — click-free on MediaPlayer - Pop avoidance: `setVolume(0,0)` (or `s`) BEFORE prepare/play. ExoPlayer has a documented unresolved start-pop (#2752, ~200ms at full gain) — MediaPlayer does not. DECISION for design: station playback via ExoPlayer/Media3 (ICY metadata + robust HTTP reconnect, pop risk accepted/mitigated by muted preroll) vs MediaPlayer for everything (current service already uses MediaPlayer for stations successfully — simplest: KEEP MediaPlayer, it already works and is pop-free; ExoPlayer only if ICY/reconnect proves necessary). Bundled WAV fallback stays MediaPlayer. - The alarm must NOT touch audio_service/just_audio/media-session surfaces (wrong semantics, fights the alarm FGS) ### Current native service is 80% there `PluriWaveAlarmService.startAlarm` already: wake lock → startForeground BEFORE audio → 3-stage source fallback (station → fallback station → WAV, 15s timeouts) → MediaPlayer on USAGE_ALARM → linear fade (`startFadeIn`, 250ms steps, 5% start). Gaps vs target: (1) fade curve is linear-amplitude, replace with dB curve @50ms; (2) per-alarm `volume` must become the ceiling scalar of the curve (already passed via intent extras incl. EXTRA_FADE_IN_SECONDS — wiring verified end to end); (3) the handoff teardown (`confirmFlutterAudio` → stop) must DIE: service plays until stop/snooze; (4) manual audio focus request/abandon; (5) notification channel currently plays DEFAULT_ALARM_ALERT_URI on top of the player (channel has sound, notification not silent) — must be silenced so the only audio is the player; (6) ring-state event to Dart so the screen can reflect state (optional, screen is display-only). ### What gets DELETED (from the code map) - Dart ring-audio orchestration: `_prearrancarAudioAlarma` (app.dart:376-391), `_iniciarFadeIn`/`_aplicarVolumenGlobal`/`_forzarVolumenMediaUnaVez`/`_confirmarAudioFlutterListo`/`_silenciarAudio` audio parts + `_fallbackPlayer` (pantalla_alarma_sonando.dart) - Channel surface: `confirmarAudioFlutter`, `forzarVolumenMediaParaAlarma`, `restaurarVolumenMedia` (PuertoAlarmasAndroid + ServicioAlarmasAndroid + MainActivity handlers + fakes) - Kotlin: `overrideMediaVolumeForRing`/`restoreMediaVolume`/`restoreMediaVolumeBestEffort` + companion state (MainActivity), `flutterOwnsRing` flag + backstops (PluriWaveAlarmService) - Tests that die: ramp/handoff/override groups in pantalla_alarma_sonando_test.dart (172-291), media-volume-restore group in dismiss_guard_test (360-429) ### What must NOT change (do-not-break list from the map) - Entire scheduling stack: AlarmScheduler.kt scheduling/persistence/boot, ServicioAlarmas/ServicioProgramacionAlarmas, EstadoAlarmas CRUD/snooze-truth-ownership/skip/vacations, PluriWaveBootReceiver - Receiver ACTION_FIRE ordering (re-arm → service → activity); wake-lock/startForeground-before-audio ordering - Snooze flows (5 entry points, truth-ownership directions documented in map §1), pre-notice + snooze-countdown notification chains, AlarmNotificationStrings/NotificationBrand - Radio/audio_service/EQ/sleep-timer/recording/BT — only their INCIDENTAL use by the ring goes away. Regression guard test (EstadoRadio never touches override channel) becomes the decoupling proof — keep/extend - Dismiss/snooze navigation semantics (canPop/SystemNavigator, SnackBar-on-failure) — rewrite setup, keep assertions - Ring screen keeps: buttons → EstadoAlarmas.finalizarEjecucion/posponerAlarma (native stop happens via detener/ocultar as today), PopScope back=Stop, single-exit guard ### Delivery hardening (same change or fast-follow — design decides) - Explicit PARTIAL_WAKE_LOCK at top of PluriWaveAlarmReceiver.onReceive (DeskClock pattern; currently only the service takes one) - Do NOT start mediaPlayback FGS from BOOT_COMPLETED on A15+ (boot receiver only reschedules — verify current code complies) - Runtime status surface: canScheduleExactAlarms / isIgnoringBatteryOptimizations / canUseFullScreenIntent(API34+) — EstadoAlarmas.cargarDiagnostico exists; extend + in-app guidance screen incl. MIUI autostart deep link (`miui.intent.action.OP_AUTO_START`, launch-only, not queryable) → SEPARATE follow-up change (oem-reliability-guidance) ## Product semantics (communicated to user) - "Volumen 50%" = 50% relative to the device ALARM stream knob (like Google Clock). Media volume 0 → rings regardless (free). Alarm stream 0 → silent like the system clock (accepted; optional override NOT adopted) - EQ does not apply during the ring (native player has no EQ pipeline); normal radio unaffected - Dismiss restores nothing because nothing was changed ## User device facts (POCO X7 Pro "rodin", HyperOS, v0.1.93 tested) - Standard Android layers all green (deviceidle whitelist ✓, standby bucket 5=EXEMPTED ✓, RUN_ANY_IN_BACKGROUND allow ✓, alarms registered exact with Alarm-clock sections ✓) - Remaining delivery suspect: HyperOS Autostart (Ajustes → Aplicaciones → Administrar aplicaciones → PluriWave → Inicio automático) + MIUI battery saver "Sin restricciones" — user enabling now (Fase 1) ## Testability - Dart: scheduling tests all survive; ring-screen tests rewritten as pure-UI (buttons → EstadoAlarmas calls, navigation, SnackBar); channel-shape tests for the REDUCED PuertoAlarmasAndroid surface - Kotlin: no test harness (unchanged) — the dB-curve math could be extracted to a pure function; on-device QA is the real gate: fade audible curve, volume ceiling, no pop, dismiss/snooze/back, screen-off delivery post-Autostart