Close the SDD cycle: verified pass with warnings (0 critical), slice 1 cancelled with SDK evidence, slices 2-3 shipped, post-verify dispose fix landed. The native-alarms capability spec is promoted to openspec/specs/ with the corrected FGS requirement. Phase 5 on-device QA remains the pending human gate.
67 lines
4.8 KiB
Markdown
67 lines
4.8 KiB
Markdown
# Proposal: Alarm Volume Ramp & Device-Volume Immunity
|
|
|
|
## Intent
|
|
|
|
An alarm must ring regardless of device media volume. Today the alarm hands off within 1-3s from the volume-immune native `USAGE_ALARM` player to the Flutter media-stream player, which is fully governed by media volume — so **media volume 0 = silent alarm** for nearly the whole ring. A manifest bug (`systemExempted` instead of `alarm`, no `FOREGROUND_SERVICE_ALARM`) can also silently kill the native start on Android 14+. Device-volume immunity for the full ring was never actually built; it must be built now.
|
|
|
|
## Scope
|
|
|
|
### In Scope
|
|
- **Manifest fix**: `PluriWaveAlarmService` → `foregroundServiceType="mediaPlayback|alarm"` + declare `FOREGROUND_SERVICE_ALARM`. Restores already-approved design (`app-quality-and-native-alarms` D1.1 / S1-R1).
|
|
- **Ring-scoped media-volume override (Kotlin-owned)**: on ring start capture `STREAM_MUSIC` volume, force an audible reference level; player ramps 5% → `alarma.volumen` (existing Dart ramp); restore original volume on EVERY exit (dismiss, snooze, dispose, service teardown, best-effort on kill).
|
|
- **Fade-in dedup**: gate native + Dart ramps so only one drives audible volume across handoff.
|
|
|
|
### Out of Scope
|
|
- Full native-only audio for the whole ring (exploration option b) — **future follow-up**.
|
|
- Single-sourcing the fade-in constant/curve shared by Kotlin+Dart — noted as debt.
|
|
|
|
## Capabilities
|
|
|
|
### New Capabilities
|
|
- None.
|
|
|
|
### Modified Capabilities
|
|
- `native-alarms`: alarm ring MUST be immune to device media volume for its full duration (not just the pre-handoff window); FGS type/permission corrected; fade-in single-driver across handoff.
|
|
|
|
## Approach
|
|
|
|
Override lives in **Kotlin** (`MainActivity`/`PluriWaveAlarmService` via a new method on the existing `pluriwave/alarm_scheduler` MethodChannel) because `AudioManager.setStreamVolume` has no Flutter plugin here and MainActivity already owns audio channels. **Dart drives the ring lifecycle**; `pantalla_alarma_sonando.dart` calls `capture+override` at ring start and `restore` from the already-centralized exit points (`_silenciarAudio` → dismiss/snooze; `dispose`). Restore must be **idempotent** and provably scoped: normal radio listening and phone-call ducking (`ServicioAudioSession`, S3-R1) stay untouched — the override only fires while a ring is active. Existing 5%→target Dart ramp is kept; the native ramp is gated to avoid a second audible driver at handoff.
|
|
|
|
## Affected Areas
|
|
|
|
| Area | Impact | Description |
|
|
|------|--------|-------------|
|
|
| `android/.../AndroidManifest.xml:57` | Modified | FGS type `mediaPlayback\|alarm` + `FOREGROUND_SERVICE_ALARM` permission |
|
|
| `android/.../MainActivity.kt` | Modified | New `alarm_scheduler` methods: capture/override/restore `STREAM_MUSIC` |
|
|
| `android/.../PluriWaveAlarmService.kt` | Modified | Restore-on-teardown safety net; gate native fade-in vs Dart |
|
|
| `lib/pantallas/pantalla_alarma_sonando.dart` | Modified | Invoke override at start; restore in `_silenciarAudio`/`dispose` |
|
|
| `lib/servicios/servicio_alarmas_android.dart` | Modified | Dart wrapper for the new channel methods |
|
|
| `lib/app.dart` | Modified | Coordinate override with early `_prearrancarAudioAlarma` |
|
|
|
|
## Risks
|
|
|
|
| Risk | Likelihood | Mitigation |
|
|
|------|------------|------------|
|
|
| Kotlin/manifest not compilable by agent | High | On-device verify on Android 14+ is a MANDATORY human gate (CC-R1/R2) |
|
|
| App killed mid-ring leaves volume overridden | Med | Best-effort restore in service teardown + `onDestroy`; document residual gap |
|
|
| Override leaks into normal radio playback | Med | Scope strictly to ring; idempotent restore; assert ducking path untouched |
|
|
| Double fade-in still audible at handoff | Low | Single-driver gate; test the handoff window |
|
|
|
|
## Rollback Plan
|
|
|
|
Per-slice and independent. Revert the manifest line to restore prior FGS type. Behind a guard so `restore` always runs even if `override` failed (no worse than today). If regressions appear in normal playback, disable the override call in `pantalla_alarma_sonando.dart` — manifest fix stands alone.
|
|
|
|
## Dependencies
|
|
|
|
- Android 14+ device for the mandatory on-device verification of the manifest fix.
|
|
- Confirm from user whether `app-quality-and-native-alarms` Slice 1 (manifest) was ever verified on a real device (apply-progress deviation note is missing).
|
|
|
|
## Success Criteria
|
|
|
|
- [ ] Manifest declares `mediaPlayback|alarm` + `FOREGROUND_SERVICE_ALARM`; native service starts on Android 14+.
|
|
- [ ] With device media volume at 0, the alarm still rings and ramps 5% → `alarma.volumen`.
|
|
- [ ] Original media volume is restored after dismiss, snooze, and dispose.
|
|
- [ ] Normal radio listening and phone-call ducking are unaffected.
|
|
- [ ] Only one fade-in driver is audible across the native→Flutter handoff.
|
|
- [ ] No new user-visible strings (no l10n across 13 locales required).
|