Files
pluriwave/openspec/changes/alarm-volume-ramp-restore/specs/native-alarms/spec.md
T
FreeTLab 43f61d7c21
Build & Deploy PluriWave / Análisis de código (push) Successful in 34s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m43s
docs(openspec): cancel alarm FGS slice, the alarm service type never existed
Apply-stage SDK verification (javap on android-34/35/36 platform jars
plus api-versions.xml) proved FOREGROUND_SERVICE_TYPE_ALARM and the
FOREGROUND_SERVICE_ALARM permission are fictional constants. The
existing mediaPlayback|systemExempted declaration is the documented
correct pattern for an alarm app holding exact-alarm permissions, so
slice 1 ships no code and root cause B is withdrawn. Spec, design,
and tasks amended with the evidence; volume-override and fade-dedup
slices proceed unaffected.
2026-07-11 01:22:18 +02:00

5.5 KiB

Delta for native-alarms

ADDED Requirements

Requirement: Manifest declares alarm-eligible foreground service

CORRECTED (2026-07-11, apply-stage SDK verification): the originally specified foregroundServiceType="mediaPlayback|alarm" + FOREGROUND_SERVICE_ALARM permission are FICTIONAL — neither constant exists anywhere in the Android SDK (verified via javap -constants on android-34/35/36 platform jars and api-versions.xml; 13 real FGS types, no alarm variant). The existing declaration foregroundServiceType="mediaPlayback|systemExempted" is the documented correct pattern for an alarm-clock app holding exact-alarm permissions that starts a foreground service from an AlarmManager broadcast on API 34+. The prior app-quality-and-native-alarms deviation (T-S1-03/04) that kept systemExempted was right. No manifest or runtime change ships in this change; the requirement is re-scoped to the service-start behavior below.

Scenario: Manifest keeps the verified-correct FGS declaration

Testability: static/grep-verifiable

  • GIVEN the built AndroidManifest.xml
  • WHEN the PluriWaveAlarmService <service> element is inspected
  • THEN android:foregroundServiceType MUST be mediaPlayback|systemExempted
  • AND the runtime startForeground type constants in PluriWaveAlarmService.kt MUST match the manifest declaration

Scenario: Native service starts from broadcast context on Android 14+

Testability: manual on-device QA (Android 14+; agent cannot install/verify)

  • GIVEN an alarm is scheduled and the app is not foregrounded
  • WHEN PluriWaveAlarmReceiver.onReceive(ACTION_FIRE) starts PluriWaveAlarmService
  • THEN it MUST start without ForegroundServiceTypeException
  • AND the alarm MUST be audible via the native USAGE_ALARM player

Requirement: Ring-scoped device-volume override

While an alarm is ringing, the system MUST override STREAM_MUSIC to an audible reference level so the alarm is not silenced by device volume 0, and MUST restore the original captured volume when the ring ends through any exit path.

Scenario: Alarm is audible when device media volume is 0

Testability: manual on-device QA (native volume APIs)

  • GIVEN device STREAM_MUSIC volume is 0
  • WHEN an alarm fires and the override captures/raises STREAM_MUSIC to an audible level
  • THEN the alarm MUST be audible for the full ring duration, not only the pre-handoff window

Scenario: Player fade-in ramps within the overridden level

Testability: flutter test with fakes

  • GIVEN the override has set an audible STREAM_MUSIC reference level
  • WHEN the ring's fade-in timer runs
  • THEN player volume MUST ramp 5% -> alarma.volumen over alarma.fadeInSegundos, unchanged from today

Scenario: Dismiss restores the original captured volume

Testability: flutter test with fakes (Dart->channel call) + manual on-device QA (native restore)

  • GIVEN the override captured STREAM_MUSIC at volume V
  • WHEN the user dismisses the alarm (_detener() -> _silenciarAudio())
  • THEN STREAM_MUSIC MUST be restored to exactly V

Scenario: Snooze restores the original captured volume

Testability: flutter test with fakes (Dart->channel call) + manual on-device QA (native restore)

  • GIVEN the override captured STREAM_MUSIC at volume V
  • WHEN the user snoozes the alarm (_posponer() -> _silenciarAudio())
  • THEN STREAM_MUSIC MUST be restored to exactly V

Scenario: Restore is idempotent across double-exit paths

Testability: flutter test with fakes (single/no-op restore call assertion) + manual on-device QA

  • GIVEN restore already ran once (_silenciarAudio() inside _detener())
  • WHEN a second exit path also runs restore (e.g. dispose() firing after)
  • THEN the second call MUST NOT throw, MUST NOT re-apply a stale value, and MUST leave STREAM_MUSIC unchanged

Scenario: Normal radio playback never triggers the override

Testability: flutter test with fakes (channel never invoked outside a ring) + manual on-device QA

  • GIVEN the user is listening to radio with no alarm ringing
  • WHEN playback starts, plays, or stops normally
  • THEN the override MUST NOT be invoked; STREAM_MUSIC stays fully governed by device controls

Scenario: App killed mid-ring — best-effort restore only

Testability: manual on-device QA; accepted residual gap, not required in automated coverage

  • GIVEN an alarm is ringing and the override is active
  • WHEN the app process is killed before any exit path runs
  • THEN the system SHOULD best-effort restore from service teardown/onDestroy, but a residual overridden-volume state MAY occur and is an accepted known gap

Requirement: Single fade-in driver across native-to-Flutter handoff

During handoff from the native USAGE_ALARM player to the Flutter media-stream player, only one fade-in ramp MUST drive audible volume at any instant.

Scenario: No double-ramp interleaving at handoff

Testability: manual on-device QA (timing-sensitive, cross-process); flutter test with fakes can assert the Dart ramp only runs when native ownership is inactive, but cannot observe native MediaPlayer.setVolume() timing

  • GIVEN the native fade-in is ramping the USAGE_ALARM player
  • WHEN the Flutter player becomes ready and handoff occurs (confirmarAudioFlutter)
  • THEN the native fade-in MUST stop driving audible volume once Flutter takes over; both ramps MUST NOT drive audible volume simultaneously

Non-Functional Notes

  • No new user-visible strings; no l10n work required across the 13 supported locales.