Files
pluriwave/openspec/specs/native-alarms/spec.md
T
FreeTLab efbf289f6b
Build & Deploy PluriWave / Análisis de código (push) Successful in 39s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m48s
docs(openspec): archive alarm-volume-ramp-restore and promote native-alarms spec
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.
2026-07-11 10:32:07 +02:00

5.6 KiB

Native Alarms Specification

Purpose

Native (Kotlin) foreground-service behavior for ringing alarms: reliable start from a broadcast context on Android 14+ via the correct foreground-service-type declaration, ring-scoped device-volume immunity so an alarm is audible even when device media volume is 0, and a single audible fade-in driver across the native-to-Flutter playback handoff.

Requirements

Requirement: Manifest declares alarm-eligible foreground service

foregroundServiceType="mediaPlayback|systemExempted" on the PluriWaveAlarmService <service> element in AndroidManifest.xml is the correct, verified declaration for an alarm-clock app holding exact-alarm permissions that starts a foreground service from an AlarmManager broadcast on API 34+. FOREGROUND_SERVICE_TYPE_ALARM and android.permission.FOREGROUND_SERVICE_ALARM do NOT exist anywhere in the Android SDK (verified via javap -constants on android-34/35/36 platform jars and a full api-versions.xml sweep — 13 real FGS types, no alarm variant); do not attempt to introduce them.

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.