Files
pluriwave/openspec/changes/app-quality-and-native-alarms/apply-progress.md
T
FreeTLab 079e19f0ee feat(audio): audio session integration and runtime robustness
- 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
2026-06-11 16:25:09 +02:00

30 KiB
Raw Blame History

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 analyzeNo 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; 0xFFFFB86Btokens.warmCoral; blurSigma: 10 + cold-GPU comment (Design 2.4)
T-S2b-04 [x] GREEN: lib/tema/pluri_animate.dartpluriFadeIn/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.dartmusic().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 D1D5 / S1S5)

Defect Fix Where
D1 (audit S1) — native snooze never notifies Flutter ACTION_SNOOZEMainActivity.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.