Files
pluriwave/openspec/changes/archive/2026-07-12-native-alarm-ring/proposal.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

5.9 KiB

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.