f3e9487215
- Use mediaPlayback|systemExempted FGS type with FOREGROUND_SERVICE_SYSTEM_EXEMPTED so alarms fire on Android 14+ (FOREGROUND_SERVICE_ALARM does not exist in the SDK) - Deduplicate fire notifications: the foreground service FSI notification is the single owner; receiver path removed - Notification channel v2 with alarm sound URI and USAGE_ALARM attributes, one-time guarded migration from legacy channels - Pass fallback station through the MethodChannel (NativeAlarmSpec schemaVersion 3) with a three-stage audio chain: primary -> fallback station -> bundled WAV - Native fade-in volume ramp honoring fadeInSegundos when the app is killed - Request battery-optimization exemption once, tracked with a persisted asked-once flag - Fix snooze end-to-end: native ACTION_SNOOZE now reports back to Flutter (snoozed event + cold-start sync), snooze anchor unified to occurrence+minutes on both sides, periodic recalc no longer erases an active snooze - Add snooze buttons (3/5/10/custom) to the ringing screen with shared audio teardown - Redesign ringing screen on PluriWaveScaffold with reduced-motion-aware entry animation (new PluriAnimate helper) - Alarm editor: live next-trigger preview, searchable station pickers (primary and fallback), configurable snooze duration, volume floor down to 0 - New alarm strings localized across all 13 locales - New unit/widget tests for the snooze flow, alarm bridge payloads, ringing screen and editor (77 tests green) - SDD artifacts for the app-quality-and-native-alarms change (explore, proposal, spec, design, tasks, apply progress)
470 lines
42 KiB
Markdown
470 lines
42 KiB
Markdown
# Tasks: app-quality-and-native-alarms
|
||
|
||
## Review Workload Forecast
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Estimated changed lines (total) | ~1 850–2 100 |
|
||
| 400-line budget risk (overall) | High — all slices combined |
|
||
| Chained PRs recommended | N/A (local apply — no PRs) |
|
||
| Suggested split | S1 → S2a → S2b → S3a → S3b → S7 → S4a → S4b → S5 → S6 |
|
||
| Delivery strategy | auto-chain |
|
||
| Chain strategy | N/A (local apply — user commits at own cadence) |
|
||
|
||
Decision needed before apply: No
|
||
Chained PRs recommended: N/A (local apply)
|
||
Chain strategy: N/A (local apply)
|
||
400-line budget risk: High
|
||
|
||
> **Per-slice risks** are noted inline. Each slice is an autonomous apply batch; the
|
||
> user reviews and commits before the next slice begins.
|
||
|
||
### Suggested Work Units (apply batches)
|
||
|
||
| Batch | Slices | Goal | Prerequisite | Est. lines |
|
||
|-------|--------|------|--------------|------------|
|
||
| 1 | S1 | Native alarm reliability (manifest, FSI, channels, fallback, fade) | — | ~330 |
|
||
| 2 | S2a | Snooze correctness: bridge sync + ringing-screen buttons | S1 complete | ~260 |
|
||
| 3 | S2b | Editor redesign + visual (next-trigger, station picker, snooze field, scaffold) | S2a complete | ~180 |
|
||
| 4 | S3a | Test seams: statics→instance, prefs injection, cache/mutex, dirty-guard, bounded set | S2 complete | ~270 |
|
||
| 5 | S3b | audio_session integration + becoming-noisy + intent flag seam | S3a complete | ~100 |
|
||
| 6 | S7 | Streaming resilience: buffer config, reconnect state machine, UI wiring | S3b complete | ~285 |
|
||
| 7 | S4a | ServicioExportImport + EstadoEcualizador extraction + compat getters | S3 complete | ~350 |
|
||
| 8 | S4b | EstadoGrabacion + EstadoBusqueda + context.select rewiring + remove compat getters | S4a complete | ~380 |
|
||
| 9 | S5 | Design system, a11y, i18n, polish | S2b complete | ~210 |
|
||
| 10 | S6 | Quality gates: analysis_options + top-5 tests + lint fix-ups | S4b + S5 complete | ~120 |
|
||
|
||
---
|
||
|
||
## Slice S1 — Alarm native reliability (~330 lines)
|
||
|
||
> **Verification verbs** — on-device items are deferred to the user's device checklist (Section 11).
|
||
> Dart items: `flutter test`, `flutter analyze`, `dart format`.
|
||
|
||
### S1 pre-work: write failing tests
|
||
|
||
- [x] **T-S1-01** [RED] Write failing test: `test/servicios/servicio_alarmas_android_test.dart` — assert `programar()` MethodChannel payload contains keys `fallbackStationUrl`, `fallbackStationName`, `fadeInSegundos`, `fallbackSound`. **Reqs:** S1-R4, S1-R6. **~20 lines.**
|
||
- [x] **T-S1-02** [RED] Write failing test in same file — assert `solicitarExencionBateria()` invokes `requestIgnoreBatteryOptimizations` on the MethodChannel. **Reqs:** S1-R5. **~15 lines.**
|
||
|
||
### S1 implementation: Kotlin / manifest (on-device verification)
|
||
|
||
- [x] **T-S1-03** Edit `android/app/src/main/AndroidManifest.xml`: add `<uses-permission android:name="android.permission.FOREGROUND_SERVICE_ALARM"/>` near line 5; change `PluriWaveAlarmService` to `android:foregroundServiceType="mediaPlayback|alarm"` (lines 54-57). **Reqs:** S1-R1. *On-device verify deferred to user.* **DEVIATION:** `alarm` FGS type / `FOREGROUND_SERVICE_ALARM` permission do NOT exist in the Android SDK (verified against android-36 `android.jar`); implemented with `systemExempted` / `FOREGROUND_SERVICE_SYSTEM_EXEMPTED`, the documented type for alarm-clock apps holding `SCHEDULE_EXACT_ALARM`/`USE_EXACT_ALARM`.
|
||
- [x] **T-S1-04** Edit `android/app/src/main/kotlin/.../PluriWaveAlarmService.kt` line ~75: on API ≥ 34 call `startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_ALARM)`; on API < 34 keep 2-arg overload. **Reqs:** S1-R1. *On-device verify.* **DEVIATION:** uses `FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED` (see T-S1-03; `FOREGROUND_SERVICE_TYPE_ALARM` does not exist).
|
||
- [x] **T-S1-05** Edit `PluriWaveAlarmReceiver.kt`: remove `showFireNotification` call (lines 37, 95-133). The service `startForeground` notification (ID 92841) is now the single owner of the FSI. Keep `fireNotificationIdForAlarm` helper for `cancelAlarm` migration safety — do NOT post to it. **Reqs:** S1-R2. *On-device verify.*
|
||
- [x] **T-S1-06** Edit `PluriWaveAlarmService.kt` `buildNotification`: add `setFullScreenIntent(...)` so the FSI appears instantly at `startForeground` before audio prepares. Ensure `stopAlarm` (line ~224) calls `stopForeground(STOP_FOREGROUND_REMOVE)` and also cancels any legacy `fireNotificationIdForAlarm` id as migration guard. **Reqs:** S1-R2. *On-device verify.* (`setFullScreenIntent` and both `stopAlarm` guards were already present; verified and documented ordering with a comment.)
|
||
- [x] **T-S1-07** Edit `PluriWaveAlarmService.kt` (~line 374) and `PluriWaveAlarmReceiver.kt` (~line 269): introduce versioned channel id `pluriwave_alarm_fire_v2` (IMPORTANCE_HIGH) with `setSound(DEFAULT_ALARM_ALERT_URI, USAGE_ALARM AudioAttributes)` and `enableVibration(true)`. Add one-time channel migration: delete `pluriwave_alarm_native` and `pluriwave_alarm_fire` guarded by SharedPreferences flag `channels_migrated_v2`. Service's `startForeground` notification now uses `_fire_v2`. **Reqs:** S1-R3. *On-device verify.*
|
||
- [x] **T-S1-08** Edit `AlarmScheduler.kt` `NativeAlarmSpec` (lines 571-648): add `fallbackStationName: String?`, `fallbackStationUrl: String?`, `fadeInSegundos: Int` fields; bump `schemaVersion` 2→3; update `toJson`/`fromJson` (additive, defaults null/0 for missing fields). Wire through `scheduleAlarm` signature, `MainActivity` handler (lines 68-106), and `EXTRA_*` constants / `fireIntent` extras. **Reqs:** S1-R4, S1-R6. *On-device verify.*
|
||
- [x] **T-S1-09** Edit `PluriWaveAlarmService.kt` `startAudio` (lines 86-108): implement three-stage ordered fallback state machine (primary station 15s → fallback station 15s → bundled WAV). Reuse `scheduleStationFallback`/`cancelStationFallback` per stage. **Reqs:** S1-R4. *On-device verify.*
|
||
- [x] **T-S1-10** Edit `PluriWaveAlarmService.kt` `setOnPreparedListener` (lines 128-136, 179-183): if `fadeInSegundos > 0`, start at 0.05 × target volume and step every 250 ms toward `volume` via `mainHandler` runnable. Cancel ramp runnable in `stopAlarm` and on snooze. **Reqs:** S1-R6. *On-device verify.*
|
||
- [x] **T-S1-11** Add `requestIgnoreBatteryOptimizations` MethodChannel handler in `MainActivity.kt` (mirror `requestExactAlarmPermission` ~lines 255-270): launch `Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`. **Reqs:** S1-R5. *On-device verify.*
|
||
|
||
### S1 implementation: Dart bridge
|
||
|
||
- [x] **T-S1-12** [GREEN] Edit `lib/servicios/servicio_alarmas_android.dart` `programar()` (lines 148-174): add `fallbackStationUrl`, `fallbackStationName` (from `alarma.emisoraFallback`), and `fadeInSegundos` to the MethodChannel args map. **Reqs:** S1-R4, S1-R6. **~15 lines.**
|
||
- [x] **T-S1-13** [GREEN] Add `solicitarExencionBateria()` method to `PuertoAlarmasAndroid` interface and `ServicioAlarmasAndroid` implementation (`lib/servicios/servicio_alarmas_android.dart` ~lines 93-107, 196-218): invoke `requestIgnoreBatteryOptimizations` MethodChannel. **Reqs:** S1-R5. **~20 lines.**
|
||
- [x] **T-S1-14** [GREEN] Edit `lib/estado/estado_alarmas.dart` `_solicitarPermisosNecesariosParaAlarma` (lines 268-284): call `android.solicitarExencionBateria()` ONLY when `!diag.ignoraOptimizacionBateria` AND a `bateria_exencion_solicitada` flag is unset in SharedPreferences (asked-once guard). **Reqs:** S1-R5. **~15 lines.**
|
||
|
||
### S1 verification
|
||
|
||
- [x] **T-S1-15** Run `flutter test test/servicios/servicio_alarmas_android_test.dart` — T-S1-01, T-S1-02 must pass (GREEN). Verify T-S1-12, T-S1-13, T-S1-14 output. (Full suite: 54 tests passing.)
|
||
- [x] **T-S1-16** Run `flutter analyze` — zero errors. (`No issues found!`, identical to pre-S1 baseline.)
|
||
- [x] **T-S1-17** Run `dart format lib/servicios/servicio_alarmas_android.dart lib/estado/estado_alarmas.dart`. (Also formatted both touched test files.)
|
||
|
||
### S1 Definition of Done
|
||
- `flutter test` green (T-S1-01, T-S1-02 passing; no regressions in existing 12 test files).
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied to all edited Dart files.
|
||
- Reqs checked off: S1-R1 (on-device), S1-R2 (on-device), S1-R3 (on-device), S1-R4 (Dart portion), S1-R5 (Dart portion), S1-R6 (Dart portion).
|
||
- User performs on-device verification (see Section 11) for the Kotlin/manifest tasks before starting S2.
|
||
|
||
---
|
||
|
||
## Slice S2a — Snooze correctness (~260 lines)
|
||
|
||
> Covers Design Decisions 2.1–2.3. Must ship before S2b.
|
||
|
||
### S2a pre-work: write failing tests
|
||
|
||
- [x] **T-S2a-01** [RED] Create `test/estado/estado_alarmas_snooze_test.dart`:
|
||
- Test A: `posponerAlarma(alarma, 5)` calls `android.programar` once with `snoozeHasta = proximaEjecucion + 5 min`; calls `notifyListeners`. (S2-R6-A, S2-R1)
|
||
- Test B: A `snoozed` native event triggers `servicio.posponerEjecucionHasta` + `notifyListeners` WITHOUT a second `android.programar`. (S2-R3, Decision 2.1)
|
||
- Test C: `recalcularTodas` called after `posponerAlarma` PRESERVES `snoozeHasta` (S4 regression guard). (S2-R6)
|
||
**DONE — plus extra tests: cold-start snooze import, stop-cancels-snooze (S2-R5), finalizarEjecucion clears snooze. Shared fake moved to `test/helpers/fakes_alarmas.dart`.**
|
||
- [x] **T-S2a-02** [RED] Create `test/servicios/servicio_alarmas_snooze_test.dart`:
|
||
- Test A: `posponerEjecucionHasta(id, origin, until)` computes `snoozeHasta = origin + minutes` and persists. (S2-R6)
|
||
- Test B: MethodChannel payload for a snoozed alarm contains `snoozeUntilMillis` matching `snoozeHasta`. (S2-R6)
|
||
- Test C: `finalizarEjecucion` clears `snoozeHasta` and calls `android.cancelar` (or `programar` without `snoozeHasta`). (S2-R5, S2-R6)
|
||
**DONE — Test C lives in `estado_alarmas_snooze_test.dart` (it is EstadoAlarmas behavior). Added anchor-clamp and custom-minutes tests + `getNativeSnoozeState` bridge tests.**
|
||
- [x] **T-S2a-03** [RED] Add test in `test/estado/estado_alarmas_snooze_test.dart`: after `posponerAlarma`, the alarm list in the state reflects updated `snoozeHasta` synchronously (no poll wait). (S2-R2) **DONE.**
|
||
|
||
### S2a implementation: Kotlin native→Flutter sync (on-device portion)
|
||
|
||
- [x] **T-S2a-04** Edit `PluriWaveAlarmService.kt` snooze handler (`ACTION_SNOOZE`, now lines 56-80): after `AlarmScheduler.snooze(...)` (which now returns `NativeSnoozeResult`), calls `MainActivity.notifyAlarmEvent` with `alarmAction="snoozed"`, `occurrenceAtMillis`, `snoozeUntilMillis`, title and minutes. **Reqs:** S2-R3, Decision 2.1. *On-device verify.*
|
||
- [x] **T-S2a-05** `MainActivity.kt` companion `notifyAlarmEvent(payload)` (lines ~610-635): posts `alarmFired` on the main handler through a `@Volatile activeInstance` (set in `configureFlutterEngine`, cleared in `onDestroy`); no-op with log when engine dead. **Reqs:** S2-R3. *On-device verify.*
|
||
- [x] **T-S2a-06** `AlarmScheduler.kt` `snooze()` (lines 266-292): anchor unified to `occurrenceAt + minutes` clamped to `now + minutes` (postponeNext logic adopted; also persists `snoozeMinutes`); returns `NativeSnoozeResult` for the bridge callback. **Reqs:** S2-R4, Decision 2.2. *On-device verify.*
|
||
- [x] **T-S2a-07** `AlarmScheduler.kt` `nativeSnoozeStates()` (lines 366-385) returns active future snoozes (alarmId + snoozeUntilMillis + snoozeOriginMillis); wired as `getNativeSnoozeState` in `MainActivity` (line 192). **Reqs:** S2-R3, Decision 2.1 engine-dead case. *On-device verify.*
|
||
|
||
### S2a implementation: Dart bridge and state
|
||
|
||
- [x] **T-S2a-08** [GREEN] `EventoAlarmaAndroid` extended with `snoozeUntilMillis` field and `accionSnoozed` const; `app.dart` `_abrirAlarmaSonando` ignores `snoozed` events (EstadoAlarmas owns them). **DONE.**
|
||
- [x] **T-S2a-09** [GREEN] `programar()` already sent `snoozeUntilMillis`/`snoozeOriginMillis` (pre-existing); now LOCKED by test (`servicio_alarmas_snooze_test.dart` payload test). **Reqs:** S2-R6. **No code change needed.**
|
||
- [x] **T-S2a-10** [GREEN] `EstadoAlarmas` subscribes to `android.eventosAlarma` in the CONSTRUCTOR (not `inicializar` — see deviations); `_alRecibirEventoNativo` (estado_alarmas.dart:266) records the snooze via `posponerEjecucionHasta` + `_aplicar` + `notifyListeners`, with NO second `android.programar`. Subscription cancelled in `dispose`. **Reqs:** S2-R3, S2-R2. **DONE.**
|
||
- [x] **T-S2a-11** [GREEN] `_sincronizarEjecucionesGestionadasPorAndroid` now always ends with `_importarSnoozesNativosActivos()` (estado_alarmas.dart:306,312): imports active future native snoozes for active alarms when they differ from the stored value. **Reqs:** S2-R3. **DONE.**
|
||
- [x] **T-S2a-12** [GREEN] `obtenerEstadoSnoozeNativo()` added to `PuertoAlarmasAndroid` + impl invoking `getNativeSnoozeState`; new `EstadoSnoozeNativo` model with `fromMap`. **DONE.**
|
||
- [x] **T-S2a-13** [GREEN] S2-R5 implemented in `servicio_alarmas.dart` `_recalcular` (line 395): `snoozeActivo` now requires `alarma.activa`, so disabling an alarm clears its snooze; `finalizarEjecucion` already cleared it via `completarEjecucion` and re-programs without snooze through `_sincronizarTodas` (the real bridge cancels natively for inactive alarms). Both paths covered by tests. **Reqs:** S2-R5. **DONE.**
|
||
|
||
### S2a implementation: ringing screen snooze buttons
|
||
|
||
- [x] **T-S2a-14** [RED] Widget tests in `test/pantallas/pantalla_alarma_sonando_test.dart`:
|
||
- Test A: snooze buttons 3/5/10 + custom 7 present; no-dup test when snoozeMinutos=5. (S2-R1-A, S2-R1-C)
|
||
- Test B: tapping 5-min snooze records snoozeHasta, pauses audio, hides the native notification and pops. (S2-R1-B)
|
||
**DONE.**
|
||
- [x] **T-S2a-15** [GREEN] `_liberarAudioLocal()` (pantalla_alarma_sonando.dart:138) cancels `_fallbackTimer`/`_fadeInTimer`, cancels `_estadoSub` (fire-and-forget — see deviations), stops `_fallbackPlayer`; `_posponer(int)` (line 161) = teardown → `radio.audio.pausar()` → `posponerAlarma` → pop; `_detener` refactored to reuse it. **Reqs:** S2-R1. **DONE.**
|
||
- [x] **T-S2a-16** [GREEN] Snooze button row (`_opcionesSnooze()` = sorted {3,5,10,custom}) rendered with `l10n.alarmSnoozeOptionLabel(min)` + `l10n.snoozeAction` header, each wired to `_posponer`. New ARB keys added to ALL 13 locales. **Reqs:** S2-R1-A/B/C. **DONE.**
|
||
|
||
### S2a verification
|
||
|
||
- [x] **T-S2a-17** `flutter test` on the three snooze test files — all green (RED phase captured first: compile failures + anchor mismatch).
|
||
- [x] **T-S2a-18** `flutter test` (full suite) — 77/77 passing, no regressions.
|
||
- [x] **T-S2a-19** `flutter analyze` — `No issues found!`.
|
||
- [x] **T-S2a-20** `dart format` applied to all touched Dart files (lib + test).
|
||
|
||
### S2a Definition of Done
|
||
- `flutter test` green (new snooze tests passing; 12 existing files unbroken).
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S2-R1, S2-R2, S2-R3 (Dart portion), S2-R4 (Kotlin deferred), S2-R5, S2-R6.
|
||
|
||
---
|
||
|
||
## Slice S2b — Editor + visual redesign (~180 lines)
|
||
|
||
> Covers Design Decisions 2.4–2.5.
|
||
|
||
### S2b pre-work: write failing tests
|
||
|
||
- [x] **T-S2b-01** [RED] `test/pantallas/pantalla_alarma_sonando_scaffold_test.dart`: asserts `PluriWaveScaffold` present; no `Scaffold` with `Color(0xFF061722)`; `Animate` present normally and ABSENT with `disableAnimations=true`. **Reqs:** S2-R7, S5-R3. **DONE.**
|
||
- [x] **T-S2b-02** [RED] `test/pantallas/pantalla_alarmas_editor_test.dart` (5 tests):
|
||
- Test A: next-trigger preview present (key `next-trigger-preview`) and changes when weekday recurrence changes (Mon→Tue, date-independent). (S2-R8)
|
||
- Test B: station field opens bottom sheet with `SearchBar`; typing filters the list. (S2-R9)
|
||
- Test B2: fallback-station field opens the same picker. (S2-R9)
|
||
- Test C: snooze SegmentedButton present; selecting 10 + save persists `snoozeMinutos = 10`. (S2-R10)
|
||
- Test D: volume slider min is 0.0. (S2-R11)
|
||
**DONE.**
|
||
|
||
### S2b implementation
|
||
|
||
- [x] **T-S2b-03** [GREEN] Ringing screen migrated to `PluriWaveScaffold`; `Color(0xFF061722)` removed, `Color(0xFFFFB86B)` → `tokens.warmCoral`; `blurSigma` capped to 10 with cold-GPU comment (Design 2.4 mitigation). **Reqs:** S2-R7, S5-R1 (partial). **DONE.**
|
||
- [x] **T-S2b-04** [GREEN] `lib/tema/pluri_animate.dart` created: `pluriFadeIn`/`pluriScaleIn` returning the child untouched when `MediaQuery.maybeDisableAnimationsOf(context)` is true. **Reqs:** S5-R3. **DONE.**
|
||
- [x] **T-S2b-05** [GREEN] Glass surface wrapped in `.pluriFadeIn(context)` entry animation. **Reqs:** S2-R7. **DONE.**
|
||
- [x] **T-S2b-06** [GREEN] `_vistaProximaEjecucion` in the editor: computes `calcularProxima` from the in-progress draft (respects vacations/exceptions), renders `alarmNextExecution`/`alarmNoNextExecution`, recomputed on every `setState` so it tracks time/recurrence edits live. **Reqs:** S2-R8. **DONE.**
|
||
- [x] **T-S2b-07** [GREEN] `DropdownButtonFormField` replaced by `_CampoSelectorEmisora` + `_SelectorEmisoraSheet` (bottom sheet with `SearchBar` over favorites + "no station" option); second identical picker added for `emisoraFallback` (NEW field in the editor). `AlarmaMusical.copyWith` gained `limpiarEmisora`/`limpiarEmisoraFallback` so "none" actually clears. **Reqs:** S2-R9. **DONE.**
|
||
- [x] **T-S2b-08** [GREEN] Snooze duration SegmentedButton (3/5/10 + current custom value) writing `_snoozeMinutos` (saved via `copyWith(snoozeMinutos: ...)` — the editor previously hardcoded 5 for new alarms); volume slider floor lowered 0.25 → 0.0 (divisions 20). **Reqs:** S2-R10, S2-R11. **DONE.**
|
||
|
||
### S2b verification
|
||
|
||
- [x] **T-S2b-09** `flutter test` on both S2b test files — 7/7 green (RED captured first).
|
||
- [x] **T-S2b-10** `flutter test` (full suite) — 77/77 passing, no regressions.
|
||
- [x] **T-S2b-11** `flutter analyze` — `No issues found!`.
|
||
- [x] **T-S2b-12** `dart format` applied to all touched files.
|
||
|
||
### S2b Definition of Done
|
||
- `flutter test` green.
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S2-R7, S2-R8, S2-R9, S2-R10, S2-R11.
|
||
|
||
---
|
||
|
||
## Slice S3a — Test seams: statics, prefs, cache, mutex, bounded set (~270 lines)
|
||
|
||
> Covers Design Decisions 3.2, 3.3, 3.4, 3.5. Must complete before S3b and S7 (S7 depends on the intent flag seam from 3.1, which is in S3b).
|
||
|
||
### S3a pre-work: write failing tests
|
||
|
||
- [ ] **T-S3a-01** [RED] Create `test/servicios/servicio_alarmas_android_instance_test.dart`: two `ServicioAlarmasAndroid` instances do not share `_eventosController` (S3-R2-A). Use a fake `MethodChannel`. **~20 lines.**
|
||
- [ ] **T-S3a-02** [RED] Create `test/servicios/servicio_alarmas_cache_test.dart`:
|
||
- Test A: `recalcularTodas` does NOT call `SharedPreferences.setString` when schedule unchanged (S3-R5-A).
|
||
- Test B: `recalcularTodas` calls `SharedPreferences.setString` exactly once when changed (S3-R5-B).
|
||
- Test C: Two concurrent `guardarAlarma` calls produce consistent final state — no lost write (S3-R7-A, S6-R2 test #1 preview). **~50 lines.**
|
||
- [ ] **T-S3a-03** [RED] Create `test/estado/estado_alarmas_ejecuciones_test.dart`: `_ejecucionesEmitidas` with 100 stale entries triggers pruning; length ≤ max threshold after prune (S3-R6-A). **~20 lines.**
|
||
- [ ] **T-S3a-04** [RED] Create `test/widgets/mini_reproductor_configurar_test.dart`: `configurarLocalizaciones` called at most once per locale change across 10 rebuilds (S3-R3-A). **~20 lines.**
|
||
|
||
### S3a implementation
|
||
|
||
- [ ] **T-S3a-05** [GREEN] Edit `lib/servicios/servicio_alarmas_android.dart` (lines 117-120): convert `static _eventosController`, `static _handlerInstalado`, `static _l10n` to INSTANCE fields. Install handler in constructor. Add deprecated static shim for `estado_radio.dart:74` call site (one-release compat). Rewire `EstadoRadio.configurarLocalizaciones` to call the instance. **Reqs:** S3-R2. **~40 lines.**
|
||
- [ ] **T-S3a-06** [GREEN] Edit `lib/widgets/mini_reproductor.dart` (line 23): convert to `StatefulWidget` if not already; move `configurarLocalizaciones(l10n)` call to `didChangeDependencies`, guarded by a cached `Locale` comparison so it only runs on locale change. **Reqs:** S3-R3. **~25 lines.**
|
||
- [ ] **T-S3a-07** [GREEN] Edit `lib/main.dart`: resolve `SharedPreferences.getInstance()` ONCE before `runApp`; pass the instance through to providers / service constructors. **Reqs:** S3-R4. **~10 lines.**
|
||
- [ ] **T-S3a-08** [GREEN] Audit and edit `lib/servicios/servicio_ecualizador.dart`, `lib/servicios/servicio_grabacion_radio.dart`, and any remaining service calling `SharedPreferences.getInstance()` inline (~25 sites): replace with injected `prefs` parameter. Use `_resolverPrefs` fallback in `servicio_alarmas.dart:399-400` as temporary compat net during migration. **Reqs:** S3-R4. **~30 lines total across files.**
|
||
- [ ] **T-S3a-09** [GREEN] Edit `lib/servicios/servicio_alarmas.dart` (lines 316-323): add dirty-check in `recalcularTodas` — serialize new config; compare to loaded serialized; skip `_guardar` if identical. Return loaded config unchanged when clean. **Reqs:** S3-R5. **~20 lines.**
|
||
- [ ] **T-S3a-10** [GREEN] Edit `lib/servicios/servicio_alarmas.dart` (lines 81-108): introduce in-memory `ConfiguracionAlarmas?` cache and a `Future`-chain mutex (mirror `_colaCambioFuente` pattern from `servicio_audio.dart:125`). All mutations: `await _lock` → read cache → mutate → persist → update cache → release. Remove `cargar()` calls before each mutation. **Reqs:** S3-R7. **~50 lines.**
|
||
- [ ] **T-S3a-11** [GREEN] Edit `lib/estado/estado_alarmas.dart` (line 32): replace unbounded `Set<String> _ejecucionesEmitidas` with a bounded structure (cap ~200 entries); add pruning of entries with millis suffix older than 24 h on each `_vigilarAlarmasVencidas` pass (lines 326-348). **Reqs:** S3-R6. **~25 lines.**
|
||
|
||
### S3a verification
|
||
|
||
- [ ] **T-S3a-12** Run `flutter test test/servicios/servicio_alarmas_android_instance_test.dart test/servicios/servicio_alarmas_cache_test.dart test/estado/estado_alarmas_ejecuciones_test.dart test/widgets/mini_reproductor_configurar_test.dart`.
|
||
- [ ] **T-S3a-13** Run `flutter test` (full suite) — no regressions.
|
||
- [ ] **T-S3a-14** Run `flutter analyze` — zero errors.
|
||
- [ ] **T-S3a-15** Run `dart format` on all edited Dart files.
|
||
|
||
### S3a Definition of Done
|
||
- `flutter test` green.
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S3-R2, S3-R3, S3-R4, S3-R5, S3-R6, S3-R7.
|
||
|
||
---
|
||
|
||
## Slice S3b — audio_session + becoming-noisy + intent flag (~100 lines)
|
||
|
||
> Provides the `_intencionReproducir` flag seam that S7 requires.
|
||
|
||
### S3b pre-work: write failing tests
|
||
|
||
- [ ] **T-S3b-01** [RED] Create `test/servicios/servicio_audio_session_test.dart`:
|
||
- Test A: interruption `begin/pause` event sets `_intencionReproducir` to false and pauses playback. (S3-R1)
|
||
- Test B: interruption `end/shouldResume` resumes playback. (S3-R1)
|
||
- Test C: becoming-noisy event pauses playback. (S3-R1)
|
||
**~30 lines.**
|
||
|
||
### S3b implementation
|
||
|
||
- [ ] **T-S3b-02** [GREEN] Create `lib/servicios/servicio_audio_session.dart`: `ServicioAudioSession` wrapper around `package:audio_session`. In `configurar()`: `AudioSession.instance` → configure with `AudioSessionConfiguration.music()` adjusted (playback category, `androidWillPauseWhenDucked: true`). Subscribe to `interruptionEventStream` (pause/duck/resume) and `becomingNoisyEventStream` (pause). On interrupt begin: call `handler.pause()` + set `handler._intencionReproducir = false`. On end with `shouldResume`: call `handler.play()` + set `handler._intencionReproducir = true`. **Reqs:** S3-R1. **~60 lines.**
|
||
- [ ] **T-S3b-03** [GREEN] Edit `lib/servicios/servicio_audio.dart` `PluriWaveAudioHandler`: expose `_intencionReproducir` flag (bool, default false). Set true in `play()`/`reproducir()`/`reanudar()`; set false in `pause()`/`detener()`. This is the seam S7 will read. Wire `ServicioAudioSession.configurar()` call from `main.dart` or `PluriWaveAudioHandler` init. **Reqs:** S3-R1. **~20 lines.**
|
||
|
||
### S3b verification
|
||
|
||
- [ ] **T-S3b-04** Run `flutter test test/servicios/servicio_audio_session_test.dart`.
|
||
- [ ] **T-S3b-05** Run `flutter test` (full suite) — no regressions.
|
||
- [ ] **T-S3b-06** Run `flutter analyze` — zero errors.
|
||
- [ ] **T-S3b-07** Run `dart format lib/servicios/servicio_audio_session.dart lib/servicios/servicio_audio.dart`.
|
||
|
||
### S3b Definition of Done
|
||
- `flutter test` green.
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S3-R1 (`flutter analyze` import present; on-device call-pause deferred to user).
|
||
|
||
---
|
||
|
||
## Slice S7 — Streaming resilience (~285 lines)
|
||
|
||
> Depends on S3b (intent flag seam). Covers Design Decisions 7.1–7.2.
|
||
|
||
### S7 pre-work: write failing tests
|
||
|
||
- [ ] **T-S7-01** [RED] Create `test/servicios/servicio_audio_reconnect_test.dart`:
|
||
- Test A: backoff delay sequence for retries 1–5 is [1s, 2s, 4s, 8s, 16s] capped at maxDelay (S7-R7).
|
||
- Test B: `_intencionReproducir=true` + stall → `reconectando` state emitted, reconnect scheduled (S7-R2-A, S7-R7).
|
||
- Test C: `_intencionReproducir=false` + stall → NO reconnect (S7-R2-B, S7-R7).
|
||
- Test D: after `maxRetries` exhausted → error state emitted (S7-R2-C, S7-R7).
|
||
- Test E: successful reconnect resets retry counter (S7-R7).
|
||
- Test F: user stop during stall cancels reconnect (S7-R6, S7-R7).
|
||
**~70 lines.**
|
||
- [ ] **T-S7-02** [RED] Add test in `test/servicios/servicio_audio_reconnect_test.dart`: buffer config (`AndroidLoadControl`) applied to player construction (S7-R1). **~15 lines.**
|
||
- [ ] **T-S7-03** [RED] Add widget test `test/widgets/reconnect_ui_test.dart`: no `AlertDialog`/`SnackBar` shown while handler in `reconectando` state (S7-R3-A). **~20 lines.**
|
||
|
||
### S7 implementation
|
||
|
||
- [ ] **T-S7-04** [GREEN] Edit `lib/servicios/servicio_audio.dart` `_crearPlayer` (lines 159-163): pass `AudioLoadConfiguration(androidLoadControl: AndroidLoadControl(minBufferDuration: 15s, maxBufferDuration: 50s, bufferForPlaybackDuration: 2.5s, bufferForPlaybackAfterRebufferDuration: 5s, prioritizeTimeOverSizeThresholds: true))` at construction. Values extracted as named constants, NOT magic literals. **Reqs:** S7-R1. **~25 lines.**
|
||
- [ ] **T-S7-05** [GREEN] Edit `lib/servicios/servicio_audio.dart`: add `EstadoReproduccion.reconectando` to the state enum (line 14). **Reqs:** S7-R2, S7-R3. **~3 lines.**
|
||
- [ ] **T-S7-06** [GREEN] Edit `lib/servicios/servicio_audio.dart` `_gestionarErrorReproduccion` (lines 207-236) and `_eventosSub.onError` (lines 189-194): instead of transitioning immediately to terminal error when `_intencionReproducir == true` and error is network-class (2xxx range), enter the reconnect state machine — emit `reconectando`, schedule backoff retry using `_cambiarFuente` revision guard. Cancel/reset on user stop or source switch. After `maxRetries` exhaustion fall through to existing terminal error path. Configurable: `_maxRetries = 5`, `_baseDelay = 1s`, `_maxDelay = 30s`. **Reqs:** S7-R2. **~130 lines.**
|
||
- [ ] **T-S7-07** [GREEN] Edit `lib/widgets/mini_reproductor.dart` and any player UI: map `EstadoReproduccion.reconectando` → buffering/loading indicator (NOT error dialog). **Reqs:** S7-R3. **~20 lines.**
|
||
- [ ] **T-S7-08** [GREEN] Edit `lib/pantallas/pantalla_alarma_sonando.dart` (alarm pre-start / estadoStream listener): ensure `reconectando` is NOT treated as `reproduciendo`; the alarm's existing 12-second fallback timer remains authoritative. Add a comment documenting the boundary. **Reqs:** S7-R4. **~10 lines.**
|
||
- [ ] **T-S7-09** [GREEN] Confirm `ServicioGrabacionRadio` error-handling code is NOT modified by S7 changes. Add inline comment referencing S7-R5 invariant. **Reqs:** S7-R5. **~3 lines (comment only).**
|
||
|
||
### S7 verification
|
||
|
||
- [ ] **T-S7-10** Run `flutter test test/servicios/servicio_audio_reconnect_test.dart test/widgets/reconnect_ui_test.dart`.
|
||
- [ ] **T-S7-11** Run `flutter test` (full suite) — no regressions.
|
||
- [ ] **T-S7-12** Run `flutter analyze` — zero errors.
|
||
- [ ] **T-S7-13** Run `dart format lib/servicios/servicio_audio.dart lib/widgets/mini_reproductor.dart lib/pantallas/pantalla_alarma_sonando.dart`.
|
||
|
||
### S7 Definition of Done
|
||
- `flutter test` green (all reconnect tests passing).
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S7-R1 (Dart buffer config), S7-R2, S7-R3, S7-R4, S7-R5, S7-R6, S7-R7; on-device stream-drop deferred to user.
|
||
|
||
---
|
||
|
||
## Slice S4a — ServicioExportImport + EstadoEcualizador (~350 lines)
|
||
|
||
> Extraction order: ServicioExportImport first (pure logic, zero UI coupling), then EstadoEcualizador.
|
||
|
||
### S4a pre-work: write failing tests
|
||
|
||
- [ ] **T-S4a-01** [RED] Create `test/servicios/servicio_export_import_test.dart`:
|
||
- Test A: full round-trip (favorites, groups, EQ, alarms, vacations) — serialize then deserialize produces deep-equal config. (S4-R4-A, S6-R2 test #4)
|
||
- Test B: malformed JSON input to `importar()` → graceful empty result, no throw. (S4-R4)
|
||
**~40 lines.**
|
||
- [ ] **T-S4a-02** [RED] Create `test/estado/estado_ecualizador_test.dart`:
|
||
- Test A: `aplicarPreset` notifies `EstadoEcualizador` listeners. (S4-R1-A)
|
||
- Test B: `EstadoRadio` listeners are NOT rebuilt on EQ preset change. (S4-R1-A, S4-R5)
|
||
**~30 lines.**
|
||
|
||
### S4a implementation
|
||
|
||
- [ ] **T-S4a-03** [GREEN] Create `lib/servicios/servicio_export_import.dart`: `ServicioExportImport` class with `exportar(config) → String` and `importar(json) → ConfiguracionCompleta?`. Move all `jsonEncode`/`jsonDecode` backup/restore logic from `lib/pantallas/pantalla_ajustes.dart` (~1391 lines). `PantallaAjustes` delegates to this service. **Reqs:** S4-R4. **~100 lines (service) + ~30 lines cleanup in pantalla_ajustes.**
|
||
- [ ] **T-S4a-04** [GREEN] Create `lib/estado/estado_ecualizador.dart`: `EstadoEcualizador extends ChangeNotifier` — owns preset, bands, enabled flag. Move EQ state fields and methods from `lib/estado/estado_radio.dart`. Add `ProxyProvider` registration in `MultiProvider` (wherever providers are registered, likely `lib/main.dart` or `lib/app.dart`). **Reqs:** S4-R1. **~90 lines.**
|
||
- [ ] **T-S4a-05** [GREEN] Edit `lib/estado/estado_radio.dart`: add backward-compatible getters delegating EQ state to `EstadoEcualizador` (transition bridge). These are removed in S4b. Add `// TODO(S4b): remove getter` comments. **~20 lines.**
|
||
- [ ] **T-S4a-06** [GREEN] Edit `lib/widgets/ecualizador_widget.dart` and `lib/pantallas/pantalla_ajustes.dart` EQ sections: consume `context.watch<EstadoEcualizador>()` (scoped). Screens still compile via compat getters if missed. **Reqs:** S4-R5. **~20 lines.**
|
||
|
||
### S4a verification
|
||
|
||
- [ ] **T-S4a-07** Run `flutter test test/servicios/servicio_export_import_test.dart test/estado/estado_ecualizador_test.dart`.
|
||
- [ ] **T-S4a-08** Run `flutter test` (full suite) — no regressions.
|
||
- [ ] **T-S4a-09** Run `flutter analyze` — zero errors.
|
||
- [ ] **T-S4a-10** Run `dart format lib/servicios/servicio_export_import.dart lib/estado/estado_ecualizador.dart lib/estado/estado_radio.dart lib/pantallas/pantalla_ajustes.dart`.
|
||
|
||
### S4a Definition of Done
|
||
- `flutter test` green.
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S4-R1, S4-R4.
|
||
|
||
---
|
||
|
||
## Slice S4b — EstadoGrabacion + EstadoBusqueda + context.select rewiring (~380 lines)
|
||
|
||
### S4b pre-work: write failing tests
|
||
|
||
- [ ] **T-S4b-01** [RED] Create `test/estado/estado_grabacion_test.dart`: `ServicioGrabacionRadio` is managed by `EstadoGrabacion`; notifies listeners on recording state change. (S4-R2) **~20 lines.**
|
||
- [ ] **T-S4b-02** [RED] Create `test/estado/estado_busqueda_test.dart`: search query update notifies `EstadoBusqueda` listeners. (S4-R3) **~15 lines.**
|
||
- [ ] **T-S4b-03** [RED] Add widget test: changing EQ preset does NOT rebuild `PantallaInicio` (S4-R5-A). **~20 lines.**
|
||
|
||
### S4b implementation
|
||
|
||
- [ ] **T-S4b-04** [GREEN] Create `lib/estado/estado_grabacion.dart`: `EstadoGrabacion extends ChangeNotifier` — owns recording state + `_escucharGrabacion` subscription (currently `estado_radio.dart:51, :79`). Register in `MultiProvider`. **Reqs:** S4-R2. **~80 lines.**
|
||
- [ ] **T-S4b-05** [GREEN] Create `lib/estado/estado_busqueda.dart`: `EstadoBusqueda extends ChangeNotifier` — owns search query, results, loading state. Register in `MultiProvider`. **Reqs:** S4-R3. **~60 lines.**
|
||
- [ ] **T-S4b-06** [GREEN] Edit `lib/pantallas/pantalla_inicio.dart` (line 43): replace root `context.watch<EstadoRadio>()` with `context.select` / `Consumer` scoped to fields it actually reads. **Reqs:** S4-R5. **~30 lines.**
|
||
- [ ] **T-S4b-07** [GREEN] Edit `lib/pantallas/pantalla_ajustes.dart` (~6 watch sites): replace each `context.watch<EstadoRadio>()` with scoped `context.select` / `Consumer` for the specific field. **Reqs:** S4-R5. **~40 lines.**
|
||
- [ ] **T-S4b-08** [GREEN] Edit `lib/pantallas/pantalla_favoritos.dart`: scope the `EstadoRadio` watch. **Reqs:** S4-R5. **~15 lines.**
|
||
- [ ] **T-S4b-09** [GREEN] Edit `lib/estado/estado_radio.dart`: remove EQ, recording, and search state fields/methods; remove backward-compatible getters added in S4a (they carried `// TODO(S4b): remove getter` comments). **Reqs:** S4-R1, S4-R2, S4-R3. **~80 lines removed.**
|
||
|
||
### S4b verification
|
||
|
||
- [ ] **T-S4b-10** Run `flutter test test/estado/estado_grabacion_test.dart test/estado/estado_busqueda_test.dart` plus the rebuild scope test.
|
||
- [ ] **T-S4b-11** Run `flutter test` (full suite) — no regressions.
|
||
- [ ] **T-S4b-12** Run `flutter analyze` — zero errors.
|
||
- [ ] **T-S4b-13** Run `dart format lib/estado/estado_grabacion.dart lib/estado/estado_busqueda.dart lib/estado/estado_radio.dart lib/pantallas/pantalla_inicio.dart lib/pantallas/pantalla_ajustes.dart lib/pantallas/pantalla_favoritos.dart`.
|
||
|
||
### S4b Definition of Done
|
||
- `flutter test` green.
|
||
- `flutter analyze` clean.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S4-R2, S4-R3, S4-R5.
|
||
|
||
---
|
||
|
||
## Slice S5 — Design system, a11y, i18n (~210 lines)
|
||
|
||
> Parallelizable after S2b completes (ringing screen literals migrated in S2b).
|
||
|
||
### S5 pre-work: write failing tests
|
||
|
||
- [ ] **T-S5-01** [RED] Create `test/widgets/tarjeta_emisora_a11y_test.dart`: favorite `InkWell` has semantic label + `button:true`; size ≥ 48×48 dp (S5-R2-A). **~20 lines.**
|
||
- [ ] **T-S5-02** [RED] Add test in `test/tema/pluri_animate_test.dart`: `pluriFadeIn` returns unanimated child when `disableAnimations=true` (S5-R3-A). **~15 lines.**
|
||
- [ ] **T-S5-03** [RED] Create `test/pantallas/pantalla_alarmas_fecha_test.dart`: `_fechaCorta` with locale `en-US` returns `DateFormat.yMd('en-US')` result, NOT `11/06/2026` (S5-R4-A). **~15 lines.**
|
||
- [ ] **T-S5-04** [RED] Add test `test/pantallas/pantalla_favoritos_plural_test.dart`: plural form changes between 1 and 5 station count strings (S5-R5). **~10 lines.**
|
||
- [ ] **T-S5-05** [RED] Add widget test: shimmer present during loading state in `PantallaBuscar` (S5-R6). **~10 lines.**
|
||
- [ ] **T-S5-06** [RED] Add unit test: `AudioServiceConfig.notificationColor` equals brand color token (S5-R8). **~10 lines.**
|
||
|
||
### S5 implementation
|
||
|
||
- [ ] **T-S5-07** [GREEN] Edit all 14+ remaining `Color(0x...)` literal sites identified in explore C3 (files: `lib/pantallas/`, `lib/widgets/`, excluding `pantalla_alarma_sonando.dart` done in S2b): replace with `PluriWaveTokens` or `Theme.of(context).colorScheme` references. **Reqs:** S5-R1. **~30 lines across files.**
|
||
- [ ] **T-S5-08** [GREEN] Edit `lib/widgets/tarjeta_emisora.dart` (lines 238-289): wrap mini favorite `InkWell` in `Semantics(button: true, label: l10n.toggleFavorite)`; set `constraints: BoxConstraints(minWidth: 48, minHeight: 48)`. Add `semanticLabel` to `_AssetIcon`/alarm PNG. **Reqs:** S5-R2. **~15 lines.**
|
||
- [ ] **T-S5-09** [GREEN] `lib/tema/pluri_animate.dart` already created in S2b (T-S2b-04). Verify tests pass (no new code needed here unless edge case found).
|
||
- [ ] **T-S5-10** [GREEN] Edit `lib/pantallas/pantalla_alarmas.dart` `_fechaCorta` (line 1114): replace hardcoded format string with `intl.DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(date)`. **Reqs:** S5-R4. **~5 lines.**
|
||
- [ ] **T-S5-11** [GREEN] Edit `lib/pantallas/pantalla_favoritos.dart` (line 138): replace bare counter string with ARB plural form using `AppLocalizations` `stationCount(n)` plural message. Add the ARB plural entry to `lib/l10n/*.arb` files for all supported locales. **Reqs:** S5-R5. **~20 lines (Dart) + ARB entries.**
|
||
- [ ] **T-S5-12** [GREEN] Edit `lib/widgets/tarjeta_emisora.dart` shimmer placeholders (lines 389-420): apply `BorderRadius` matching card corners. Edit `lib/pantallas/pantalla_buscar.dart` (lines 241-245): replace spinner with shimmer during loading state. **Reqs:** S5-R6. **~20 lines.**
|
||
- [ ] **T-S5-13** [GREEN] Edit `lib/pantallas/pantalla_ajustes.dart` icon sites (lines 985, 1028, 1031): replace non-`_rounded` icon variants with their `_rounded` equivalents. **Reqs:** S5-R7. **~5 lines.**
|
||
- [ ] **T-S5-14** [GREEN] Edit `lib/main.dart` (line 23) `AudioServiceConfig`: set `notificationColor` to `PluriWaveTokens.brandColor` (or equivalent token). **Reqs:** S5-R8. **~3 lines.**
|
||
|
||
### S5 verification
|
||
|
||
- [ ] **T-S5-15** Run `flutter test test/widgets/tarjeta_emisora_a11y_test.dart test/tema/pluri_animate_test.dart test/pantallas/pantalla_alarmas_fecha_test.dart test/pantallas/pantalla_favoritos_plural_test.dart`.
|
||
- [ ] **T-S5-16** Run `flutter test` (full suite) — no regressions.
|
||
- [ ] **T-S5-17** Run `flutter analyze` — zero errors (no `Color(0x...)` in modified files beyond token definitions).
|
||
- [ ] **T-S5-18** Run `dart format` on all edited files.
|
||
|
||
### S5 Definition of Done
|
||
- `flutter test` green.
|
||
- `flutter analyze` clean (no new color literals in modified files).
|
||
- `dart format` applied.
|
||
- Reqs checked off: S5-R1 through S5-R8.
|
||
|
||
---
|
||
|
||
## Slice S6 — Quality gates (~120 lines)
|
||
|
||
> Hardening pass; depends on S4b + S5 complete (all code settled before lint enforcement).
|
||
|
||
### S6 pre-work: write failing tests (top-5 required tests not yet written)
|
||
|
||
- [ ] **T-S6-01** [RED] `test/servicios/servicio_alarmas_cache_test.dart` — Test C (concurrent mutation, S6-R2 test #1): already written as T-S3a-02 Test C. Verify it is present and passing.
|
||
- [ ] **T-S6-02** [RED] `test/estado/estado_alarmas_ejecuciones_test.dart` (fire dedup, S6-R2 test #2): already written as T-S3a-03. Verify passing.
|
||
- [ ] **T-S6-03** [RED] Create `test/servicios/servicio_audio_source_switch_test.dart`: rapid `playMediaItem(A)`, `playMediaItem(B)`, `playMediaItem(C)` — only C's source active; no stale error from A/B (S6-R2 test #3). Use fake `AudioPlayer` seam. **~35 lines.**
|
||
- [ ] **T-S6-04** Confirm `test/servicios/servicio_export_import_test.dart` (S6-R2 test #4, round-trip) exists from T-S4a-01. Verify passing.
|
||
- [ ] **T-S6-05** [RED] Create `test/servicios/servicio_grabacion_radio_test.dart`: recording error clears state and releases resources; subsequent start succeeds (S6-R2 test #5, S7-R5 invariant). **~30 lines.**
|
||
|
||
### S6 implementation
|
||
|
||
- [ ] **T-S6-06** [GREEN] Edit `analysis_options.yaml`: under `linter.rules` add `cancel_subscriptions`, `close_sinks`, `unawaited_futures`, `prefer_final_locals`, `avoid_dynamic_calls`. **Reqs:** S6-R1. **~6 lines.**
|
||
- [ ] **T-S6-07** [GREEN] Fix violations surfaced by the new lint rules across `lib/` (empty catches → `developer.log`, unawaited futures → `unawaited()` or `await`, open sinks/subscriptions — ensure they are tracked and cancelled). Scope: sites already noted in design B7/B10 plus any new violations. **~30 lines across files.**
|
||
- [ ] **T-S6-08** [GREEN] Run `flutter analyze` with new rules and fix remaining violations until clean.
|
||
|
||
### S6 verification
|
||
|
||
- [ ] **T-S6-09** Run `flutter test test/servicios/servicio_audio_source_switch_test.dart test/servicios/servicio_grabacion_radio_test.dart` — green.
|
||
- [ ] **T-S6-10** Run `flutter test` (full suite) — all passing including 12 original files.
|
||
- [ ] **T-S6-11** Run `flutter analyze` — zero errors under hardened rules.
|
||
- [ ] **T-S6-12** Run `dart format` on all edited files.
|
||
|
||
### S6 Definition of Done
|
||
- `flutter test` green — all 5 required tests present and passing; 12 original files unbroken.
|
||
- `flutter analyze` clean under hardened `analysis_options.yaml`.
|
||
- `dart format` applied.
|
||
- Reqs checked off: S6-R1, S6-R2 (tests 1-5).
|
||
|
||
---
|
||
|
||
## Cross-cutting batch — state.yaml + on-device checklist
|
||
|
||
- [ ] **T-CC-01** Update `openspec/changes/app-quality-and-native-alarms/state.yaml`: set `phase: tasks-ready`, `updated: 2026-06-11`.
|
||
- [ ] **T-CC-02** After the full apply and all flutter test / analyze passes, run final `dart format lib/` sweep.
|
||
|
||
### On-device verification checklist (user — Android 14 device)
|
||
|
||
Perform after S1 and after all slices are applied. **No `flutter build` from this repo — build from IDE or `flutter run`.**
|
||
|
||
1. **Alarm fires app-killed (S1-R1, S1-R2):** kill the app; wait for a scheduled alarm; confirm `PluriWaveAlarmService` starts with no `ForegroundServiceTypeException` in logcat; exactly one notification in the tray.
|
||
2. **Alarm channel uses alarm stream (S1-R3):** lower the alarm volume to 0; raise media volume; confirm alarm sound is silent (alarm volume, not media volume).
|
||
3. **Snooze from ringing screen (S2-R1, S2-R4):** with app foreground, let alarm ring; tap 5-min snooze; confirm notification dismissed; alarm list shows `snoozeHasta = now+5min`; alarm re-fires at that time.
|
||
4. **Snooze from notification while app killed (S2-R3):** kill the app; let alarm fire to notification; tap "Posponer"; confirm system alarm icon still present; bring app to foreground — alarm list shows snoozed state WITHOUT waiting for 60-second poll.
|
||
5. **Stop cancels pending snooze (S2-R5):** snooze an alarm; before re-fire, disable the alarm from the list; confirm alarm does NOT re-fire at the snooze time.
|
||
6. **Reboot persistence (S1-R1, S2-R4):** schedule an alarm; reboot device; confirm alarm still fires at scheduled time.
|
||
7. **Fallback station attempted (S1-R4):** set primary station to an invalid URL, set `emisoraFallback` to a valid one; let alarm fire; confirm the fallback station plays (or bundled WAV if fallback also fails).
|
||
8. **Battery optimization exemption requested (S1-R5):** fresh install; grant alarm permission; confirm the battery-optimization dialog appears exactly once.
|
||
9. **Stream drop recovery (S7-R1, S7-R2):** while radio plays, briefly disable WiFi/LTE for ~10 s; confirm audio continues if buffered; on reconnect, playback resumes to live edge without error dialog; a longer drop (>30s) shows reconnecting state, eventually surfaces error after retries exhausted.
|
||
10. **Phone call pauses radio (S3-R1):** while radio plays, receive a call; confirm radio pauses/ducks; confirm it resumes after the call.
|
||
11. **No alarm regression after S7 (S7-R4):** with S7 changes applied, let an alarm fire with a non-responding URL; confirm WAV fallback fires within ~15 seconds (not delayed by reconnect loop).
|
||
|
||
---
|
||
|
||
## Per-slice estimated lines and budget risk
|
||
|
||
| Slice | Est. lines | 400-line budget risk | Notes |
|
||
|-------|-----------|----------------------|-------|
|
||
| S1 | ~330 | Medium | Kotlin edits not compilable here; on-device only |
|
||
| S2a | ~260 | Medium | Snooze correctness + ringing buttons |
|
||
| S2b | ~180 | Low | Editor + visual |
|
||
| S3a | ~270 | Medium | Test seams across multiple files |
|
||
| S3b | ~100 | Low | audio_session wrapper |
|
||
| S7 | ~285 | Medium | Reconnect state machine |
|
||
| S4a | ~350 | Medium-High | Two extractions + compat getters |
|
||
| S4b | ~380 | Medium-High | Two more extractions + rewiring |
|
||
| S5 | ~210 | Low | Design system / i18n |
|
||
| S6 | ~120 | Low | Lint rules + 2 new tests |
|
||
| **Total** | **~2 285** | **High (overall)** | Distributed across 10 local slices |
|