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.
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:foregroundServiceTypeMUST bemediaPlayback|systemExempted - AND the runtime
startForegroundtype constants inPluriWaveAlarmService.ktMUST 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)startsPluriWaveAlarmService - THEN it MUST start without
ForegroundServiceTypeException - AND the alarm MUST be audible via the native
USAGE_ALARMplayer
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_MUSICvolume is 0 - WHEN an alarm fires and the override captures/raises
STREAM_MUSICto 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_MUSICreference level - WHEN the ring's fade-in timer runs
- THEN player volume MUST ramp 5% ->
alarma.volumenoveralarma.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_MUSICat volumeV - WHEN the user dismisses the alarm (
_detener()->_silenciarAudio()) - THEN
STREAM_MUSICMUST be restored to exactlyV
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_MUSICat volumeV - WHEN the user snoozes the alarm (
_posponer()->_silenciarAudio()) - THEN
STREAM_MUSICMUST be restored to exactlyV
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_MUSICunchanged
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_MUSICstays 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_ALARMplayer - 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.