079e19f0ee
- Integrate audio_session (new servicio_audio_session.dart): incoming calls pause the radio and resume on end, headphone unplug pauses without auto-resume, permanent focus loss never auto-resumes, duck lowers volume - Add play-intent flag to ServicioAudio so interruption handling and future reconnect logic can distinguish user pause from system-driven stops - Eliminate read-modify-write race in ServicioAlarmas with an in-memory cache and single-writer queue across all mutations; recalcularTodas persists only when state actually changed - Convert ServicioAlarmasAndroid static StreamController/handler to injectable instance fields, restoring test isolation - Inject a single cached SharedPreferences from main.dart across services and state (removes 23 inline getInstance() calls) - Move configurarLocalizaciones out of MiniReproductor.build() (was running on every rebuild during playback) - Bound the alarm fire-dedup set (cap 200 entries, 24h pruning) - 12 new tests (89 total green), flutter analyze clean
280 lines
30 KiB
Markdown
280 lines
30 KiB
Markdown
# Apply Progress: app-quality-and-native-alarms
|
||
|
||
**Mode**: Strict TDD (test runner: `flutter test`)
|
||
**Artifact store**: openspec (Engram unavailable this session)
|
||
**Delivery**: auto-chain, local apply — no commits, no PRs (user commits at own cadence)
|
||
**Last updated**: 2026-06-11 (Batch 3)
|
||
|
||
## Batch log
|
||
|
||
| Batch | Slice | Status | Date |
|
||
|-------|-------|--------|------|
|
||
| 1 | S1 — Alarm native reliability | COMPLETE (Dart verified; Kotlin/manifest on-device verification deferred to user) | 2026-06-11 |
|
||
| 2 | S2a + S2b — Snooze correctness end-to-end + Alarm UX parity | COMPLETE (Dart verified; Kotlin on-device verification deferred to user) | 2026-06-11 |
|
||
| 3 | S3a + S3b — Test seams (statics/prefs/cache/mutex/dirty-guard/bounded set) + audio_session | COMPLETE (Dart-only batch; call-pause on-device verification deferred to user) | 2026-06-11 |
|
||
|
||
## Task status (cumulative)
|
||
|
||
### Slice S1 — Alarm native reliability — 17/17 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S1-01 | [x] | RED test: `programar()` payload carries `fallbackStationName/Url`, `fadeInSegundos`, `fallbackSound` |
|
||
| T-S1-02 | [x] | RED test: `solicitarExencionBateria()` invokes `requestIgnoreBatteryOptimizations` |
|
||
| T-S1-03 | [x] | Manifest FGS type + permission. **DEVIATION** (see below) |
|
||
| T-S1-04 | [x] | API ≥ 34 3-arg `startForeground` with type bitmask. **DEVIATION** (see below) |
|
||
| T-S1-05 | [x] | Receiver `showFireNotification` + `ensureFireChannel` removed; service notification (id 92841) is sole FSI owner; `fireNotificationIdForAlarm` kept for cancel-migration safety |
|
||
| T-S1-06 | [x] | `setFullScreenIntent` was ALREADY present in service `buildNotification`; `stopAlarm` already cancels legacy fire id + `stopForeground(STOP_FOREGROUND_REMOVE)`; FSI-before-audio ordering documented with comment |
|
||
| T-S1-07 | [x] | Channel `pluriwave_alarm_fire_v2` (IMPORTANCE_HIGH, `DEFAULT_ALARM_ALERT_URI` + USAGE_ALARM attrs, vibration); one-time deletion of `pluriwave_alarm_native` + `pluriwave_alarm_fire` guarded by `channels_migrated_v2` flag in device-protected prefs `pluriwave_alarm_channels`; channel count consolidated to 2 (fire_v2 + pre-notice) |
|
||
| T-S1-08 | [x] | `NativeAlarmSpec` + `fallbackStationName/Url`, `fadeInSegundos`; schemaVersion 2→3; `fromJson` backward-compatible (null/0 defaults); wired through `scheduleAlarm`, `MainActivity` handler, `EXTRA_*` consts, `fireIntent` extras |
|
||
| T-S1-09 | [x] | Three-stage chain primary(15s) → fallback station(15s) → bundled WAV via continuation lambdas (`onStageFailed`); `scheduleStationFallback` per stage with independent timeout windows |
|
||
| T-S1-10 | [x] | `startFadeIn` ramp: start 0.05×target, 250 ms steps over `fadeInSegundos`; applied to station and bundled-WAV players; `cancelFadeIn()` in `stopAlarm` (snooze path goes through `stopAlarm`) |
|
||
| T-S1-11 | [x] | `requestIgnoreBatteryOptimizations` MethodChannel handler + private method in `MainActivity.kt` mirroring `requestExactAlarmPermission` |
|
||
| T-S1-12 | [x] | GREEN: payload fields added to `programar()` args map |
|
||
| T-S1-13 | [x] | GREEN: `solicitarExencionBateria()` on `PuertoAlarmasAndroid` + impl |
|
||
| T-S1-14 | [x] | GREEN: asked-once guard in `_solicitarPermisosNecesariosParaAlarma` — calls only when `!diag.ignoraOptimizacionBateria` AND `bateria_exencion_solicitada` unset; optional `SharedPreferences? prefs` ctor param added to `EstadoAlarmas` (forward-compatible with S3 injection) |
|
||
| T-S1-15 | [x] | `flutter test` — full suite 54 tests, all passing (5 new) |
|
||
| T-S1-16 | [x] | `flutter analyze` — `No issues found!` (baseline before S1 was also clean) |
|
||
| T-S1-17 | [x] | `dart format` applied to the 4 touched Dart files |
|
||
|
||
### Slice S2a — Snooze correctness — 20/20 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S2a-01 | [x] | RED: `test/estado/estado_alarmas_snooze_test.dart` — anchor, snoozed-event, recalc-preserve + extras (cold-start import, stop-cancels-snooze, finalizar clears) |
|
||
| T-S2a-02 | [x] | RED: `test/servicios/servicio_alarmas_snooze_test.dart` — anchor future/clamped/custom(7), payload `snoozeUntilMillis`+`snoozeOriginMillis`, `getNativeSnoozeState` parse/empty. Test C (finalizar) lives in the estado file |
|
||
| T-S2a-03 | [x] | RED: synchronous list update after `posponerAlarma` (no poll) |
|
||
| T-S2a-04 | [x] | Kotlin: `ACTION_SNOOZE` reports back via `MainActivity.notifyAlarmEvent` with `alarmAction="snoozed"` (`PluriWaveAlarmService.kt:56-80`). *On-device verify* |
|
||
| T-S2a-05 | [x] | Kotlin: `MainActivity.notifyAlarmEvent` companion (lines ~610-635), `@Volatile activeInstance` set in `configureFlutterEngine`, cleared in `onDestroy`; main-thread post; no-op when engine dead. *On-device verify* |
|
||
| T-S2a-06 | [x] | Kotlin: `AlarmScheduler.snooze()` (lines 266-292) unified to `occurrenceAt + minutes` clamped to `now + minutes`; persists `snoozeMinutes`; returns `NativeSnoozeResult(until, origin, title)`. *On-device verify* |
|
||
| T-S2a-07 | [x] | Kotlin: `AlarmScheduler.nativeSnoozeStates()` (lines 366-385) + `getNativeSnoozeState` handler (`MainActivity.kt:192`). *On-device verify* |
|
||
| T-S2a-08 | [x] | GREEN: `EventoAlarmaAndroid.snoozeUntilMillis` + `accionSnoozed`; `app.dart` ignores `snoozed` events in `_abrirAlarmaSonando` |
|
||
| T-S2a-09 | [x] | `snoozeUntilMillis` was ALREADY in the `programar()` payload — locked by new test, no code change |
|
||
| T-S2a-10 | [x] | GREEN: `_alRecibirEventoNativo` (estado_alarmas.dart:266) — `posponerEjecucionHasta` + `_aplicar` + `notifyListeners`, NO second `programar`. Subscribed in the CONSTRUCTOR (deviation, see below); cancelled in `dispose` |
|
||
| T-S2a-11 | [x] | GREEN: `_importarSnoozesNativosActivos` (estado_alarmas.dart:312) called at the end of `_sincronizarEjecucionesGestionadasPorAndroid`; imports active future snoozes for active alarms when they differ |
|
||
| T-S2a-12 | [x] | GREEN: `obtenerEstadoSnoozeNativo()` on `PuertoAlarmasAndroid` + impl + `EstadoSnoozeNativo` model |
|
||
| T-S2a-13 | [x] | GREEN: `_recalcular` `snoozeActivo` now requires `alarma.activa` (servicio_alarmas.dart:395) — disabling clears the snooze; finalizar path already cleared + re-programs without snooze (bridge cancels natively when inactive) |
|
||
| T-S2a-14 | [x] | RED: `test/pantallas/pantalla_alarma_sonando_test.dart` — buttons 3/5/10(+7), no-dup, tap-5 behavior |
|
||
| T-S2a-15 | [x] | GREEN: `_liberarAudioLocal()` + `_posponer(int)` + `_detener` refactor (pantalla_alarma_sonando.dart:138,161). `_estadoSub.cancel()` is fire-and-forget (deviation, see below) |
|
||
| T-S2a-16 | [x] | GREEN: snooze button row via `_opcionesSnooze()` (sorted {3,5,10,custom}); NEW l10n keys in ALL 13 .arb files: `alarmSnoozeOptionLabel`, `snoozeAction`, `alarmSnoozeDurationTitle`, `alarmFallbackStationLabel`, `alarmStationPickerSearchHint` (+ `flutter gen-l10n` regenerated) |
|
||
| T-S2a-17 | [x] | Targeted snooze tests green |
|
||
| T-S2a-18 | [x] | Full suite 77/77 |
|
||
| T-S2a-19 | [x] | `flutter analyze` — No issues found |
|
||
| T-S2a-20 | [x] | `dart format` on touched files |
|
||
|
||
### Slice S2b — Editor + visual redesign — 12/12 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S2b-01 | [x] | RED: scaffold test — `PluriWaveScaffold` present, no `Color(0xFF061722)` Scaffold, `Animate` present / absent under `disableAnimations` |
|
||
| T-S2b-02 | [x] | RED: 5 editor tests (preview + live update, primary picker + filtering, fallback picker, snooze duration persists, volume floor 0.0) |
|
||
| T-S2b-03 | [x] | GREEN: ringing screen on `PluriWaveScaffold`; `0xFFFFB86B` → `tokens.warmCoral`; `blurSigma: 10` + cold-GPU comment (Design 2.4) |
|
||
| T-S2b-04 | [x] | GREEN: `lib/tema/pluri_animate.dart` — `pluriFadeIn`/`pluriScaleIn` honoring `MediaQuery.maybeDisableAnimationsOf` |
|
||
| T-S2b-05 | [x] | GREEN: glass surface wrapped in `.pluriFadeIn(context)` |
|
||
| T-S2b-06 | [x] | GREEN: `_vistaProximaEjecucion` (draft → `calcularProxima`, respects vacations/exceptions; recomputed on every setState) |
|
||
| T-S2b-07 | [x] | GREEN: `_CampoSelectorEmisora` + `_SelectorEmisoraSheet` (SearchBar picker) for primary AND fallback station; `copyWith` clear-flags added (see deviations) |
|
||
| T-S2b-08 | [x] | GREEN: snooze duration SegmentedButton wired to `snoozeMinutos` (editor used to hardcode 5); volume slider min 0.25 → 0.0 (divisions 20) |
|
||
| T-S2b-09 | [x] | S2b targeted tests 7/7 green |
|
||
| T-S2b-10 | [x] | Full suite 77/77 |
|
||
| T-S2b-11 | [x] | `flutter analyze` — No issues found |
|
||
| T-S2b-12 | [x] | `dart format` applied |
|
||
|
||
### Slice S3a — Test seams — 15/15 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S3a-01 | [x] | RED: `servicio_alarmas_android_instance_test.dart` — two channels, simulated `alarmFired` via `handlePlatformMessage`, isolation asserted both ways |
|
||
| T-S3a-02 | [x] | RED: `servicio_alarmas_cache_test.dart` — `_PrefsEspia implements SharedPreferences` (setString/getString counters); no-write-when-clean, exactly-one-write-when-dirty, concurrent-no-lost-write (+ single cache hydration) |
|
||
| T-S3a-03 | [x] | RED: `estado_alarmas_ejecuciones_test.dart` — 100 stale entries pruned (1 fresh survives) + 250-entry cap test |
|
||
| T-S3a-04 | [x] | RED: `mini_reproductor_configurar_test.dart` — 10 rebuilds → 1 configurar; locale es→en → 2 |
|
||
| T-S3a-05 | [x] | GREEN: `ServicioAlarmasAndroid` statics → instance fields; handler installed per instance in ctor. **DEVIATION:** no deprecated static shim (Dart name clash + only call site rewired in same change); `configurarLocalizaciones` added to `PuertoAlarmasAndroid` interface instead |
|
||
| T-S3a-06 | [x] | GREEN: `MiniReproductor` → StatefulWidget, locale-guarded `didChangeDependencies`; alarm-bridge l10n hoisted to `app.dart` `_PaginaPrincipalState.didChangeDependencies` (design 3.3 alternative), before the early-return |
|
||
| T-S3a-07 | [x] | GREEN: `main.dart` resolves prefs ONCE; `PluriWaveApp(prefs:)` → `EstadoRadio`/`EstadoAlarmas`(→`ServicioAlarmas`)/`EstadoIdioma` |
|
||
| T-S3a-08 | [x] | GREEN: injected-with-fallback `_resolverPrefs()` in `estado_radio` (10 sites), `servicio_ecualizador` (6), `servicio_grabacion_radio` (4), `servicio_contenido_app` (3). rg check: only main.dart + one fallback per class remain |
|
||
| T-S3a-09 | [x] | GREEN: `recalcularTodas` dirty-guard — serialized comparison vs `_cacheRaw`, skips write when identical |
|
||
| T-S3a-10 | [x] | GREEN: `_cache`/`_cacheRaw` + `_enCola` writer queue; all 8 mutation methods queued over `_configActual()`. **DEVIATION:** public `cargar()` still re-reads prefs (queued cache reset) because `EstadoRadio.importarConfig` writes the raw alarms key directly — a fully cached cargar would hide imports until restart |
|
||
| T-S3a-11 | [x] | GREEN: bounded `_ejecucionesEmitidas` — cap 200 + 24 h retention, pruned on every add and each `_vigilarAlarmasVencidas` pass; `@visibleForTesting` length getter |
|
||
| T-S3a-12 | [x] | Targeted S3a tests green (RED first: 1 passed / 6 failed across the batch) |
|
||
| T-S3a-13 | [x] | Full suite 89/89 (77 baseline + 12 new) |
|
||
| T-S3a-14 | [x] | `flutter analyze` — No issues found |
|
||
| T-S3a-15 | [x] | `dart format` on 19 touched files |
|
||
|
||
### Slice S3b — audio_session + intent flag — 7/7 complete
|
||
|
||
| Task | Status | Notes |
|
||
|------|--------|-------|
|
||
| T-S3b-01 | [x] | RED: `servicio_audio_session_test.dart` — 5 tests (pause-begin, resume-end, no-resume-without-prior-pause, duck begin/end, becoming-noisy hard pause) over fake `ObjetivoAudioInterrumpible` |
|
||
| T-S3b-02 | [x] | GREEN: `lib/servicios/servicio_audio_session.dart` — `music().copyWith(androidWillPauseWhenDucked: true)`; interruption + becoming-noisy subscriptions; `_pausadoPorInterrupcion` gate for auto-resume; defines `ObjetivoAudioInterrumpible` (test seam) |
|
||
| T-S3b-03 | [x] | GREEN: `PluriWaveAudioHandler implements ObjetivoAudioInterrumpible`; `_intencionReproducir` true in `play()`/`playMediaItem()`, false in `pause()`/`stop()` (S7 seam); duck via `setAtenuado` ×0.3 (`_volumenEfectivo`); `configurar()` wired in `main.dart` |
|
||
| T-S3b-04 | [x] | Targeted run 5/5 green (RED first: load failure) |
|
||
| T-S3b-05 | [x] | Full suite 89/89 |
|
||
| T-S3b-06 | [x] | `flutter analyze` — No issues found |
|
||
| T-S3b-07 | [x] | `dart format` applied |
|
||
|
||
### Remaining slices (not started)
|
||
|
||
S7, S4a, S4b, S5, S6, cross-cutting (T-CC-01, T-CC-02) — all pending.
|
||
|
||
## Snooze defect fixes (design audit D1–D5 / S1–S5)
|
||
|
||
| Defect | Fix | Where |
|
||
|--------|-----|-------|
|
||
| D1 (audit S1) — native snooze never notifies Flutter | `ACTION_SNOOZE` → `MainActivity.notifyAlarmEvent("snoozed", origin, until)`; Flutter records via `posponerEjecucionHasta` WITHOUT re-programming; engine-dead case covered by `getNativeSnoozeState` cold-start import | `PluriWaveAlarmService.kt:56-80`, `MainActivity.kt:627`, `estado_alarmas.dart:266,312` |
|
||
| D2 (audit S2) — two snooze anchors | Unified everywhere to `occurrence + minutes` clamped to `now + minutes`: native `snooze()` adopts `postponeNext` logic; Dart `posponerEjecucion` re-anchored from `now+min` to `ejecucion+min` | `AlarmScheduler.kt:266-292`, `servicio_alarmas.dart:256-274` |
|
||
| D3 (audit S3) — no snooze on ringing screen | 3/5/10 + custom buttons → `_posponer` → shared `_liberarAudioLocal` teardown → canonical `EstadoAlarmas.posponerAlarma` (Flutter-first; hides native notification = same stop path) | `pantalla_alarma_sonando.dart:138-176,242-256` |
|
||
| D4 (audit S4) — recalc erases native-only snooze | Resolved transitively by D1: Flutter now RECORDS every native snooze, so `_recalcular` sees `snoozeActivo` and preserves it; regression-guard test added; additionally snooze cleared when alarm disabled (S2-R5) | `servicio_alarmas.dart:392-401`, test `estado_alarmas_snooze_test.dart` |
|
||
| D5 (audit S5) — preserveNativeSnooze origin mismatch | Resolved transitively by D1/D2: Flutter always sends `snoozeUntilMillis` when snoozed, so the preservation net is no longer load-bearing; kept as belt-and-suspenders per design | `AlarmScheduler.kt:preserveNativeSnooze` (unchanged) |
|
||
|
||
## TDD Cycle Evidence (Strict TDD hard gate)
|
||
|
||
| Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR |
|
||
|------|-----------------------------------|-------------------------------|----------|
|
||
| T-S1-01/T-S1-12 | `servicio_alarmas_android_test.dart` written first; load failure + payload keys absent | Payload fields added; 3 tests pass | `dart format` |
|
||
| T-S1-02/T-S1-13 | Same RED run: `solicitarExencionBateria` undefined → compile failure | Interface + impl added; test passes | None needed |
|
||
| T-S1-14 | "solicita exencion una sola vez" FAILED (Expected 1, Actual 0) | Asked-once guard; both tests pass | Fake made configurable |
|
||
| T-S1-03..11 (Kotlin) | N/A — on-device items | N/A | Surgical diffs |
|
||
| T-S2a-01..03 / T-S2a-08..13 | All 3 new test files failed to LOAD (missing `EstadoSnoozeNativo`, `accionSnoozed`, `obtenerEstadoSnoozeNativo`, `alarmSnoozeOptionLabel`) — captured before any implementation. Anchor test would fail under old `now+min` semantics (verified by design: old code returned 7:05 vs expected 7:35) | All bridge/state/service changes added; targeted run 23/23 green | Shared `FakePuertoAlarmasAndroid` extracted to `test/helpers/fakes_alarmas.dart`; existing `estado_alarmas_test.dart` deduplicated |
|
||
| T-S2a-14..16 | Widget test load failure (l10n key missing) then tap-test failure (snoozeHasta null) | Buttons + `_posponer` implemented; 3/3 green | Debug prints removed; `unawaited` cancel documented |
|
||
| T-S2b-01..02 / T-S2b-03..08 | Scaffold tests failed (no `PluriWaveScaffold`/`Animate`); all 5 editor tests failed (key not found) — captured in dedicated RED run (0 passed / 7 failed) | `pluri_animate.dart`, scaffold migration, editor preview/pickers/snooze/volume; 7/7 green | `Material(transparency)` wrappers; initState-l10n fix; `dart format` |
|
||
|
||
RED run evidence: first run `+0 -3` (loading failures, three files); ringing tap test `Expected: DateTime:<07:35> Actual: <null>`; S2b run `+0 -7` before implementation. GREEN: targeted 23/23 then 7/7; full suite `00:24 +77: All tests passed!`.
|
||
|
||
### Batch 3 TDD Cycle Evidence (S3a + S3b)
|
||
|
||
| Task | RED (test written first, failing) | GREEN (implementation passes) | REFACTOR |
|
||
|------|-----------------------------------|-------------------------------|----------|
|
||
| T-S3a-01/T-S3a-05 | Static controller shared events: `expect(eventosB, isEmpty)` FAILED ('solo-a' leaked into B) | Statics → instance fields; isolation test passes | Comment documenting handler re-bind semantics |
|
||
| T-S3a-02-A/T-S3a-09 | `recalcularTodas` always wrote: `Expected: <1> Actual: <2>` writes | Dirty-guard skips clean writes | `_serializar` extracted, shared with `_guardar` |
|
||
| T-S3a-02-B | Passed pre-fix (exactly-once lock-in guard) | Still passes (regression lock) | — |
|
||
| T-S3a-02-C/T-S3a-10 | Lost write: final config had 1 of 2 alarms; 2 hydration reads | Cache + `_enCola` queue: both alarms persisted, 1 hydration read | Mutation bodies kept verbatim, only wrapped |
|
||
| T-S3a-03/T-S3a-11 | Load failure: `ejecucionesEmitidasLength`/`maxEjecucionesEmitidas` undefined | Bounded set: 1 survivor of 101, cap respected | Prune helper shared by add-path and watch-pass |
|
||
| T-S3a-04/T-S3a-06 | `Expected: <1> Actual: <11>` (configurar on every rebuild) | StatefulWidget + locale guard: 1 then 2 | FakeServicioAudio gained l10n override (assert fix) |
|
||
| T-S3b-01/02/03 | Load failure: `servicio_audio_session.dart` missing | 5/5 green against fake objetivo | — |
|
||
|
||
RED run evidence (Batch 3): `00:06 +1 -6` before implementation (the single pass is the exactly-once write lock-in). GREEN: targeted 12/12; full suite `00:12 +89: All tests passed!`.
|
||
|
||
## Files changed (Batch 2)
|
||
|
||
| File | Action | ~Lines |
|
||
|------|--------|--------|
|
||
| `android/.../AlarmScheduler.kt` | Modified | +57/-12 (unified snooze + `NativeSnoozeResult` + `nativeSnoozeStates`) |
|
||
| `android/.../MainActivity.kt` | Modified | +36/-1 (`notifyAlarmEvent` companion, `activeInstance`, `getNativeSnoozeState`) |
|
||
| `android/.../PluriWaveAlarmService.kt` | Modified | +19/-1 (snooze report-back) |
|
||
| `lib/servicios/servicio_alarmas_android.dart` | Modified | +60 (snoozed event, `EstadoSnoozeNativo`, bridge method) |
|
||
| `lib/servicios/servicio_alarmas.dart` | Modified | +20/-3 (anchor, activa-aware snooze clearing) |
|
||
| `lib/estado/estado_alarmas.dart` | Modified | +95/-15 (event subscription, snooze recording, cold-start import) |
|
||
| `lib/pantallas/pantalla_alarma_sonando.dart` | Modified | +70/-15 (snooze buttons, teardown, PluriWaveScaffold, tokens, fade-in) |
|
||
| `lib/pantallas/pantalla_alarmas.dart` | Modified | ~+330/-60 net (preview, pickers, snooze field, volume floor, initState fix, Material wrappers; large diff partly dart-format reflow) |
|
||
| `lib/modelos/alarma_musical.dart` | Modified | +10/-2 (`limpiarEmisora`/`limpiarEmisoraFallback`) |
|
||
| `lib/app.dart` | Modified | +7 (ignore snoozed events) |
|
||
| `lib/tema/pluri_animate.dart` | Created | +39 |
|
||
| `lib/l10n/app_*.arb` (13 files) | Modified | +12 each (5 keys + metadata) |
|
||
| `lib/l10n/gen/*` (15 files) | Regenerated | by `flutter gen-l10n` |
|
||
| `test/helpers/fakes_alarmas.dart` | Created | +120 |
|
||
| `test/helpers/fakes.dart` | Modified | +13 (`pausar`/`setVolumen` on FakeServicioAudio) |
|
||
| `test/estado/estado_alarmas_snooze_test.dart` | Created | +250 |
|
||
| `test/servicios/servicio_alarmas_snooze_test.dart` | Created | +155 |
|
||
| `test/pantallas/pantalla_alarma_sonando_test.dart` | Created | +165 |
|
||
| `test/pantallas/pantalla_alarma_sonando_scaffold_test.dart` | Created | +150 |
|
||
| `test/pantallas/pantalla_alarmas_editor_test.dart` | Created | +210 |
|
||
| `test/estado/estado_alarmas_test.dart` | Modified | -78/+8 (fake deduplicated to helper; anchor expectations 7:36:00 → 7:36:02) |
|
||
|
||
## Files changed (Batch 3)
|
||
|
||
| File | Action | ~Lines |
|
||
|------|--------|--------|
|
||
| `lib/servicios/servicio_audio_session.dart` | Created | +115 (session config, interruption/noisy handling, `ObjetivoAudioInterrumpible`) |
|
||
| `lib/servicios/servicio_alarmas.dart` | Modified | +106/-64 (cache, `_enCola` queue, dirty-guard, `_parsear`/`_serializar`) |
|
||
| `lib/estado/estado_alarmas.dart` | Modified | +55/-2 (bounded set, `configurarLocalizaciones`, prefs→servicio default) |
|
||
| `lib/servicios/servicio_alarmas_android.dart` | Modified | +18/-11 (statics → instance, interface method) |
|
||
| `lib/servicios/servicio_audio.dart` | Modified | +52/-3 (intent flag, `ObjetivoAudioInterrumpible` impl, duck volume) |
|
||
| `lib/estado/estado_radio.dart` | Modified | +16/-15 (prefs param + `_resolverPrefs`, static alarm-l10n call removed) |
|
||
| `lib/widgets/mini_reproductor.dart` | Modified | +22/-2 (StatefulWidget, locale-guarded didChangeDependencies) |
|
||
| `lib/main.dart` | Modified | +14/-2 (prefs once, ServicioAudioSession wiring) |
|
||
| `lib/app.dart` | Modified | +22/-3 (PluriWaveApp.prefs, alarm l10n locale guard) |
|
||
| `lib/servicios/servicio_ecualizador.dart` | Modified | +14/-6 (prefs injection) |
|
||
| `lib/servicios/servicio_grabacion_radio.dart` | Modified | +13/-4 (prefs injection) |
|
||
| `lib/servicios/servicio_contenido_app.dart` | Modified | +11/-3 (prefs injection) |
|
||
| `test/helpers/fakes.dart` | Modified | +8 (`configurarLocalizaciones` override on FakeServicioAudio) |
|
||
| `test/helpers/fakes_alarmas.dart` | Modified | +4 (interface no-op) |
|
||
| `test/servicios/servicio_alarmas_android_instance_test.dart` | Created | +53 |
|
||
| `test/servicios/servicio_alarmas_cache_test.dart` | Created | +105 |
|
||
| `test/estado/estado_alarmas_ejecuciones_test.dart` | Created | +85 |
|
||
| `test/widgets/mini_reproductor_configurar_test.dart` | Created | +85 |
|
||
| `test/servicios/servicio_audio_session_test.dart` | Created | +130 |
|
||
|
||
Total Batch 3 diff: ~362 insertions / ~122 deletions in lib + helpers, plus ~458 lines of new tests.
|
||
|
||
## Deviations from design (Batch 3)
|
||
|
||
1. **No deprecated static shim for `ServicioAlarmasAndroid.configurarLocalizaciones`** (task text asked for one). Dart forbids a static and an instance member with the same name in one class, and the ONLY external call site (`estado_radio.dart:74`) was rewired in this same slice — a renamed shim would be unreachable dead code. Instead `configurarLocalizaciones` joined the `PuertoAlarmasAndroid` interface (fakes no-op it).
|
||
2. **Alarm-bridge l10n configured from `app.dart`, not from `MiniReproductor`** — design 3.3 offered both options; the mini player now only configures its real dependency (`EstadoRadio`), and `_PaginaPrincipalState.didChangeDependencies` (locale-guarded, placed BEFORE the existing early-return) forwards l10n to `EstadoAlarmas`.
|
||
3. **Public `ServicioAlarmas.cargar()` re-reads from prefs instead of serving the cache.** `EstadoRadio.importarConfig` writes the raw `alarmas_musicales_v1` key directly to SharedPreferences; a fully cached `cargar()` would make a settings import invisible until app restart. Mutations DO use the cache (`_configActual`), which is what S3-R7's race fix and "one cargar per mutation burst" require. The re-read is queued, so it cannot interleave with a mutation.
|
||
4. **Duck handling added beyond the task text**: `setAtenuado` on the handler scales effective volume ×0.3 (restored on interruption end). With `androidWillPauseWhenDucked: true` Android delivers duck as pause, so this is mostly the iOS/edge path; kept minimal.
|
||
5. **`_PrefsEspia` implements SharedPreferences via noSuchMethod** rather than pulling `shared_preferences_platform_interface` into the tests — avoids a `depend_on_referenced_packages` lint on a transitive dep.
|
||
6. **`servicio_contenido_app.dart` also migrated** (3 getInstance sites; not named in the task). Its only construction site is `static final` in `pluri_onboarding_dialog.dart`, which keeps the fallback path at runtime — acceptable under the injected-with-fallback compat net; full injection there would require a dialog refactor out of S3 scope.
|
||
7. **Two-instances-same-channel semantics documented, not prevented**: with instance handlers, constructing a second `ServicioAlarmasAndroid` over the SAME MethodChannel re-binds the platform handler to the newest instance. Production creates exactly one per channel (provider singleton); tests use distinct channels.
|
||
|
||
## Deviations from design (Batch 2)
|
||
|
||
1. **Event subscription lives in the `EstadoAlarmas` CONSTRUCTOR**, not `inicializar` (task text said inicializar). Rationale: snoozed events must be recorded even before/without full init, and tests with `iniciarAutomaticamente: false` stay light. Cancelled in `dispose`.
|
||
2. **`posponerEjecucion` clamps minutes to 1..120 instead of coercing to 3/5/10.** The old `calcularSnooze` coercion would have made the custom snooze button (e.g. "7 min", S2-R1-C) silently snooze 5. The native notification path still sanitizes to 3/5/10 (`sanitizeSnoozeMinutes`), unchanged. `calcularSnooze` kept (unused by this path) for API compatibility.
|
||
3. **Existing test expectation updated** (`estado_alarmas_test.dart`): unified anchor makes snooze land at `proximaEjecucion(+inminencia normalization) + 5min` = 7:36:02, not `now+5` = 7:36:00. This is the spec'd behavior change (S2-R6), documented inline.
|
||
4. **`_recalcular` now clears snooze for INACTIVE alarms** — required by S2-R5-A ("snoozeHasta is null in persistent storage" after disable); previously a disabled alarm kept a stale snoozeHasta forever.
|
||
5. **`AlarmaMusical.copyWith` gained `limpiarEmisora`/`limpiarEmisoraFallback`** (not in task text). Without them the picker's "no station" could never clear an existing station (latent pre-existing bug: copyWith null-coalesced).
|
||
6. **Pre-existing debug crash fixed**: `_EditorAlarmaSheetState.initState` called `AppLocalizations.of(context)` → `dependOnInheritedWidgetOfExactType` assert in debug builds. Name controller now created lazily in `didChangeDependencies`. (The sibling `_EditorVacacionesSheet` has the same latent issue — NOT fixed here, out of S2 scope; flag for S5/S6.)
|
||
7. **`Material(type: transparency)` wrappers** added inside `PluriGlassSurface` for the editor sheet and the station picker — ListTiles inside a DecoratedBox trigger a debug assert and invisible ink splashes otherwise.
|
||
8. **`_liberarAudioLocal` does NOT await `_estadoSub.cancel()`** — a broadcast-subscription cancel future may not resolve until the stream closes (observed in tests); cancellation of delivery is synchronous, so fire-and-forget (`unawaited`) is correct and prevents the snooze tap from stalling.
|
||
9. **`AlarmScheduler.snooze` returns `NativeSnoozeResult`** (until/origin/title) so the service can build the report-back payload; `postponeNext` untouched (already had the unified anchor).
|
||
10. **Ringing screen `blurSigma` capped to 10** (PluriGlassSurface default 18) as the Design 2.4 cold-GPU mitigation, plus reduced-motion users skip the entry animation entirely.
|
||
|
||
## Issues found
|
||
|
||
- `flutter test` does NOT auto-run gen-l10n in this setup despite `generate: true`; `flutter gen-l10n` must be run manually after editing .arb files (gen files are committed).
|
||
- `tester.tap` + an awaited broadcast-subscription `cancel()` deadlocks the gesture handler chain in widget tests (see deviation 8) — worth remembering for S3/S7 work.
|
||
|
||
## On-device verification checklist for the user
|
||
|
||
From tasks.md Section 11 — S1 items still pending from Batch 1, plus new S2 items:
|
||
|
||
1. **Kotlin compiles** — native layer edited again without compilation (HIGH risk): build FIRST.
|
||
2. Alarm fires app-killed on Android 14+ (S1-R1/R2); channel v2 on alarm stream (S1-R3); 3-stage fallback (S1-R4); native fade (S1-R6); battery dialog once (S1-R5); reboot persistence.
|
||
3. **Snooze from ringing screen (S2-R1, S2-R4)**: ring → tap "5 min" → notification dismissed, list shows snoozeHasta immediately, re-fires at that time.
|
||
4. **Snooze from notification while app killed (S2-R3)**: tap "Posponer" on the fire notification with the app killed → system alarm icon persists → reopen the app → list shows the snooze WITHOUT waiting for the 60 s poll (cold-start `getNativeSnoozeState` import).
|
||
5. **Snooze from notification while app foregrounded (S2-R3)**: same, but the list updates within the same frame via the `snoozed` MethodChannel event.
|
||
6. **Stop cancels pending snooze (S2-R5)**: snooze → disable the alarm from the list → does NOT re-fire.
|
||
7. **Ringing screen visuals (S2-R7)**: PluriWaveScaffold gradient + entry fade; verify no first-frame stutter on screen-off FSI wake (blur capped); with "remove animations" accessibility setting the entry is instant.
|
||
8. **Editor (S2-R8..R11)**: next-trigger preview updates live; searchable pickers for primary AND backup station; snooze duration control; volume slider reaches 0%.
|
||
|
||
## Verification summary (Batch 2)
|
||
|
||
- `flutter test`: 77/77 passing (54 pre-batch + 23 new)
|
||
- `flutter analyze`: No issues found (identical to baseline)
|
||
- `dart format`: applied to all touched Dart files only (gen/ untouched by hand)
|
||
- `flutter gen-l10n`: run once after .arb edits
|
||
- `flutter build`: NOT run (forbidden)
|
||
|
||
## Verification summary (Batch 3)
|
||
|
||
- `flutter test`: 89/89 passing (77 baseline + 12 new across 5 files)
|
||
- `flutter analyze`: No issues found (identical to baseline)
|
||
- `dart format`: applied to all 19 touched Dart files (5 reflowed)
|
||
- `rg 'SharedPreferences.getInstance()' lib/`: only `main.dart` startup resolution + one injected-with-fallback expression per class (estado_alarmas, estado_idioma, estado_radio, servicio_alarmas, servicio_ecualizador, servicio_grabacion_radio, servicio_contenido_app)
|
||
- `flutter build`: NOT run (forbidden)
|
||
- No Kotlin/native files touched in this batch
|
||
|
||
### On-device verification items added by Batch 3 (user — Android device)
|
||
|
||
1. **Phone call pauses radio (S3-R1, checklist item 10):** while the radio plays, receive a call → radio pauses (or ducks); after the call ends it resumes automatically (transient loss).
|
||
2. **Headphones unplugged pauses radio (S3-R1):** unplug wired headphones / disconnect BT while playing → radio pauses and does NOT auto-resume.
|
||
3. **Another media app takes focus:** start playback in another app → PluriWave pauses; it must not resume on its own when focus is permanent loss.
|
||
4. **Locale switch sanity:** change app language in Ajustes → alarm titles/station names sent to new native schedules use the new language (l10n now configured per locale change, not per rebuild).
|
||
5. **Settings import still reflects alarms immediately** (cache bypass in `cargar()`): import a backup with alarms → the alarms list shows them without restarting the app.
|
||
|
||
## Workload / boundary
|
||
|
||
- Mode: auto-chain local slices (no PRs)
|
||
- Current work units: S1, S2a, S2b (committed f3e9487), S3a + S3b (complete, in working tree)
|
||
- Boundary (Batch 3): starts from the clean post-f3e9487 tree; ends with S3a+S3b fully checked off, suite green. Rollback = revert the Batch-3 files listed above (Dart-only; no native edits).
|
||
- Next batch: S7 (streaming resilience) — depends on the `_intencionReproducir` seam and `ObjetivoAudioInterrumpible` landed here. No on-device prerequisite for S7 implementation, but items 1-2 above validate the seam S7 builds on.
|