Files
pluriwave/openspec/changes/archive/2026-07-12-native-alarm-ring/explore.md
T
FreeTLab 41b95fed44
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m45s
docs(openspec): archive native-alarm-ring and update the native-alarms spec
Close the SDD cycle for the ring architecture replacement: verified
with one critical (channel silence by omission) fixed and re-checked
before archive, delta merged into the main native-alarms spec (2
requirements removed, 5 added), artifacts archived byte-for-byte.
Phase 3 on-device QA (9 items) remains the mandatory human gate.
2026-07-12 12:36:22 +02:00

56 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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