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.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user