Close the SDD cycle for the ring architecture replacement: verified with one critical (channel silence by omission) fixed and re-checked before archive, delta merged into the main native-alarms spec (2 requirements removed, 5 added), artifacts archived byte-for-byte. Phase 3 on-device QA (9 items) remains the mandatory human gate.
127 lines
6.5 KiB
Markdown
127 lines
6.5 KiB
Markdown
# 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, and sole native ring-audio ownership — `PluriWaveAlarmService` is the only audio source for the entire ring, on `STREAM_ALARM` via its own `MediaPlayer`, with an exponential dB fade-in ceiling, manually requested/abandoned transient audio focus, and a silent notification channel. System volumes are never written, so nothing ever needs restoring; the Flutter ringing screen is pure UI that only drives Stop/Snooze through `EstadoAlarmas`.
|
|
|
|
## 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: Sole native ring-audio ownership
|
|
|
|
`PluriWaveAlarmService` MUST be the only audio source for the whole ring, on `STREAM_ALARM` via its own `MediaPlayer`. No Dart player or second audible source MUST play.
|
|
|
|
#### Scenario: Media volume 0 does not silence the ring
|
|
**Testability**: on-device QA (audio routing) + flutter test with fakes
|
|
|
|
- GIVEN device `STREAM_MUSIC` volume is 0
|
|
- WHEN an alarm fires
|
|
- THEN it MUST be audible via the service's `STREAM_ALARM` player
|
|
- AND no Dart audio path (`EstadoRadio`) MUST run
|
|
|
|
#### Scenario: Fire notification posts with no sound
|
|
**Testability**: code-inspection + on-device QA
|
|
|
|
- GIVEN channel `pluriwave_alarm_fire_v3`
|
|
- WHEN the ringing notification posts
|
|
- THEN it MUST carry no sound; the player MUST be the only audible source
|
|
|
|
#### Scenario: Three-stage fallback preserved, single fade anchor
|
|
**Testability**: code-inspection + on-device QA
|
|
|
|
- GIVEN the primary station fails within its timeout
|
|
- WHEN the service advances station -> fallback station -> WAV
|
|
- THEN whichever stage starts MUST get the same fade curve, anchored at ring start
|
|
|
|
### Requirement: Exponential dB fade-in ceiling
|
|
|
|
With `fadeInSegundos > 0`: `gainDb = fraction*40-40`, `curve = 10^(gainDb/20)` on a 50ms tick, `setVolume(s * curve)`, `s` = per-alarm `volumen` as ceiling. With `fadeInSegundos == 0`: start directly at `s`.
|
|
|
|
#### Scenario: Fade rises exponentially to the ceiling
|
|
**Testability**: code-inspection (curve formula)
|
|
|
|
- GIVEN `fadeInSegundos > 0`, `volumen = s`
|
|
- WHEN the ring starts
|
|
- THEN volume at `t=0` MUST equal `s * 10^(-40/20)` (~1% of `s`), rising every 50ms
|
|
- AND volume at `t=fadeInSegundos` MUST equal exactly `s`
|
|
|
|
#### Scenario: No-fade path starts pop-free
|
|
**Testability**: code-inspection + on-device QA
|
|
|
|
- GIVEN `fadeInSegundos == 0`
|
|
- WHEN the ring starts
|
|
- THEN `setVolume(s)` MUST be applied before `start()`, with no pop
|
|
|
|
### Requirement: Manual transient focus; no system volume writes
|
|
|
|
The service MUST request `AUDIOFOCUS_GAIN_TRANSIENT` on `STREAM_ALARM` and abandon it at ring end. `setStreamVolume` MUST NOT be called on any stream, ever.
|
|
|
|
#### Scenario: Focus requested at start, abandoned at end
|
|
**Testability**: code-inspection + on-device QA (`dumpsys audio`)
|
|
|
|
- GIVEN the ring is about to start
|
|
- WHEN `startAlarm` runs
|
|
- THEN `AUDIOFOCUS_GAIN_TRANSIENT` MUST be requested on `STREAM_ALARM`
|
|
- AND WHEN the ring ends THEN focus MUST be abandoned
|
|
|
|
#### Scenario: Dismiss, snooze, and back tear down with nothing to restore
|
|
**Testability**: on-device QA + flutter test with fakes
|
|
|
|
- GIVEN an alarm is ringing
|
|
- WHEN the user dismisses, snoozes, or triggers back
|
|
- THEN the player MUST stop, focus MUST be abandoned, and foreground removed
|
|
- AND no `setStreamVolume` call MUST have occurred for this ring
|
|
|
|
### Requirement: Notification channel migration v2 -> v3
|
|
|
|
The service MUST create `pluriwave_alarm_fire_v3` (no sound) and delete `pluriwave_alarm_fire_v2` exactly once, guarded by a migration flag (same pattern as v1->v2).
|
|
|
|
#### Scenario: v2 deleted exactly once on upgrade
|
|
**Testability**: code-inspection (migration guard) + on-device QA
|
|
|
|
- GIVEN a device with `pluriwave_alarm_fire_v2` already created
|
|
- WHEN the service next ensures its channel
|
|
- THEN `_v3` MUST be created and `_v2` deleted; a re-run MUST be a no-op
|
|
|
|
### Requirement: Ring screen is pure UI
|
|
|
|
`PantallaAlarmaSonando` MUST only call `EstadoAlarmas.finalizarEjecucion`/`posponerAlarma` from Stop/Snooze/back. It MUST NOT control any audio player or volume/handoff channel. `PuertoAlarmasAndroid` MUST NOT expose `confirmarAudioFlutter`, `forzarVolumenMediaParaAlarma`, or `restaurarVolumenMedia`.
|
|
|
|
#### Scenario: Buttons and back call only EstadoAlarmas
|
|
**Testability**: flutter test with fakes
|
|
|
|
- GIVEN the ringing screen is displayed
|
|
- WHEN Stop, a Snooze option, or back is triggered
|
|
- THEN only `finalizarEjecucion`/`posponerAlarma` MUST be called
|
|
- AND zero calls MUST reach `EstadoRadio.audio` or a removed channel method
|
|
|
|
#### Scenario: Reduced Android port surface
|
|
**Testability**: static/compile-verifiable (Dart analyzer) + flutter test
|
|
|
|
- GIVEN `PuertoAlarmasAndroid` and its implementations/fakes
|
|
- WHEN inspected after this change
|
|
- THEN `confirmarAudioFlutter`, `forzarVolumenMediaParaAlarma`, `restaurarVolumenMedia` MUST NOT exist
|
|
|
|
## Non-Functional Notes
|
|
|
|
- No new user-visible strings; no l10n work required across the 13 supported locales.
|