- New EstadoGrabacion owns the recording service, subscription, directory/size preferences and open-file actions - New EstadoBusqueda owns search, nearby stations, pagination and the min-bitrate filter - New orden_emisoras.dart with the OrdenEmisoras enum, shared sorter and list identity memoization so context.select comparisons work on derived lists - Large screens (inicio, buscar, favoritos, ajustes, reproductor) consume scoped selects/dedicated notifiers instead of root context.watch<EstadoRadio>, so audio buffer events no longer rebuild whole screens - Remove all 15 TODO(S4b) compat members from EstadoRadio; consumers use the dedicated providers. EstadoRadio drops from ~1121 to 753 lines, keeping playback/stations/favorites orchestration - 8 new tests including a rebuild-scoping probe (110 total green), flutter analyze clean
48 KiB
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
- T-S1-01 [RED] Write failing test:
test/servicios/servicio_alarmas_android_test.dart— assertprogramar()MethodChannel payload contains keysfallbackStationUrl,fallbackStationName,fadeInSegundos,fallbackSound. Reqs: S1-R4, S1-R6. ~20 lines. - T-S1-02 [RED] Write failing test in same file — assert
solicitarExencionBateria()invokesrequestIgnoreBatteryOptimizationson the MethodChannel. Reqs: S1-R5. ~15 lines.
S1 implementation: Kotlin / manifest (on-device verification)
- T-S1-03 Edit
android/app/src/main/AndroidManifest.xml: add<uses-permission android:name="android.permission.FOREGROUND_SERVICE_ALARM"/>near line 5; changePluriWaveAlarmServicetoandroid:foregroundServiceType="mediaPlayback|alarm"(lines 54-57). Reqs: S1-R1. On-device verify deferred to user. DEVIATION:alarmFGS type /FOREGROUND_SERVICE_ALARMpermission do NOT exist in the Android SDK (verified against android-36android.jar); implemented withsystemExempted/FOREGROUND_SERVICE_SYSTEM_EXEMPTED, the documented type for alarm-clock apps holdingSCHEDULE_EXACT_ALARM/USE_EXACT_ALARM. - T-S1-04 Edit
android/app/src/main/kotlin/.../PluriWaveAlarmService.ktline ~75: on API ≥ 34 callstartForeground(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: usesFOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED(see T-S1-03;FOREGROUND_SERVICE_TYPE_ALARMdoes not exist). - T-S1-05 Edit
PluriWaveAlarmReceiver.kt: removeshowFireNotificationcall (lines 37, 95-133). The servicestartForegroundnotification (ID 92841) is now the single owner of the FSI. KeepfireNotificationIdForAlarmhelper forcancelAlarmmigration safety — do NOT post to it. Reqs: S1-R2. On-device verify. - T-S1-06 Edit
PluriWaveAlarmService.ktbuildNotification: addsetFullScreenIntent(...)so the FSI appears instantly atstartForegroundbefore audio prepares. EnsurestopAlarm(line ~224) callsstopForeground(STOP_FOREGROUND_REMOVE)and also cancels any legacyfireNotificationIdForAlarmid as migration guard. Reqs: S1-R2. On-device verify. (setFullScreenIntentand bothstopAlarmguards were already present; verified and documented ordering with a comment.) - T-S1-07 Edit
PluriWaveAlarmService.kt(~line 374) andPluriWaveAlarmReceiver.kt(~line 269): introduce versioned channel idpluriwave_alarm_fire_v2(IMPORTANCE_HIGH) withsetSound(DEFAULT_ALARM_ALERT_URI, USAGE_ALARM AudioAttributes)andenableVibration(true). Add one-time channel migration: deletepluriwave_alarm_nativeandpluriwave_alarm_fireguarded by SharedPreferences flagchannels_migrated_v2. Service'sstartForegroundnotification now uses_fire_v2. Reqs: S1-R3. On-device verify. - T-S1-08 Edit
AlarmScheduler.ktNativeAlarmSpec(lines 571-648): addfallbackStationName: String?,fallbackStationUrl: String?,fadeInSegundos: Intfields; bumpschemaVersion2→3; updatetoJson/fromJson(additive, defaults null/0 for missing fields). Wire throughscheduleAlarmsignature,MainActivityhandler (lines 68-106), andEXTRA_*constants /fireIntentextras. Reqs: S1-R4, S1-R6. On-device verify. - T-S1-09 Edit
PluriWaveAlarmService.ktstartAudio(lines 86-108): implement three-stage ordered fallback state machine (primary station 15s → fallback station 15s → bundled WAV). ReusescheduleStationFallback/cancelStationFallbackper stage. Reqs: S1-R4. On-device verify. - T-S1-10 Edit
PluriWaveAlarmService.ktsetOnPreparedListener(lines 128-136, 179-183): iffadeInSegundos > 0, start at 0.05 × target volume and step every 250 ms towardvolumeviamainHandlerrunnable. Cancel ramp runnable instopAlarmand on snooze. Reqs: S1-R6. On-device verify. - T-S1-11 Add
requestIgnoreBatteryOptimizationsMethodChannel handler inMainActivity.kt(mirrorrequestExactAlarmPermission~lines 255-270): launchSettings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS. Reqs: S1-R5. On-device verify.
S1 implementation: Dart bridge
- T-S1-12 [GREEN] Edit
lib/servicios/servicio_alarmas_android.dartprogramar()(lines 148-174): addfallbackStationUrl,fallbackStationName(fromalarma.emisoraFallback), andfadeInSegundosto the MethodChannel args map. Reqs: S1-R4, S1-R6. ~15 lines. - T-S1-13 [GREEN] Add
solicitarExencionBateria()method toPuertoAlarmasAndroidinterface andServicioAlarmasAndroidimplementation (lib/servicios/servicio_alarmas_android.dart~lines 93-107, 196-218): invokerequestIgnoreBatteryOptimizationsMethodChannel. Reqs: S1-R5. ~20 lines. - T-S1-14 [GREEN] Edit
lib/estado/estado_alarmas.dart_solicitarPermisosNecesariosParaAlarma(lines 268-284): callandroid.solicitarExencionBateria()ONLY when!diag.ignoraOptimizacionBateriaAND abateria_exencion_solicitadaflag is unset in SharedPreferences (asked-once guard). Reqs: S1-R5. ~15 lines.
S1 verification
- 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.) - T-S1-16 Run
flutter analyze— zero errors. (No issues found!, identical to pre-S1 baseline.) - 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 testgreen (T-S1-01, T-S1-02 passing; no regressions in existing 12 test files).flutter analyzeclean.dart formatapplied 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
- T-S2a-01 [RED] Create
test/estado/estado_alarmas_snooze_test.dart:- Test A:
posponerAlarma(alarma, 5)callsandroid.programaronce withsnoozeHasta = proximaEjecucion + 5 min; callsnotifyListeners. (S2-R6-A, S2-R1) - Test B: A
snoozednative event triggersservicio.posponerEjecucionHasta+notifyListenersWITHOUT a secondandroid.programar. (S2-R3, Decision 2.1) - Test C:
recalcularTodascalled afterposponerAlarmaPRESERVESsnoozeHasta(S4 regression guard). (S2-R6) DONE — plus extra tests: cold-start snooze import, stop-cancels-snooze (S2-R5), finalizarEjecucion clears snooze. Shared fake moved totest/helpers/fakes_alarmas.dart.
- Test A:
- T-S2a-02 [RED] Create
test/servicios/servicio_alarmas_snooze_test.dart:- Test A:
posponerEjecucionHasta(id, origin, until)computessnoozeHasta = origin + minutesand persists. (S2-R6) - Test B: MethodChannel payload for a snoozed alarm contains
snoozeUntilMillismatchingsnoozeHasta. (S2-R6) - Test C:
finalizarEjecucionclearssnoozeHastaand callsandroid.cancelar(orprogramarwithoutsnoozeHasta). (S2-R5, S2-R6) DONE — Test C lives inestado_alarmas_snooze_test.dart(it is EstadoAlarmas behavior). Added anchor-clamp and custom-minutes tests +getNativeSnoozeStatebridge tests.
- Test A:
- T-S2a-03 [RED] Add test in
test/estado/estado_alarmas_snooze_test.dart: afterposponerAlarma, the alarm list in the state reflects updatedsnoozeHastasynchronously (no poll wait). (S2-R2) DONE.
S2a implementation: Kotlin native→Flutter sync (on-device portion)
- T-S2a-04 Edit
PluriWaveAlarmService.ktsnooze handler (ACTION_SNOOZE, now lines 56-80): afterAlarmScheduler.snooze(...)(which now returnsNativeSnoozeResult), callsMainActivity.notifyAlarmEventwithalarmAction="snoozed",occurrenceAtMillis,snoozeUntilMillis, title and minutes. Reqs: S2-R3, Decision 2.1. On-device verify. - T-S2a-05
MainActivity.ktcompanionnotifyAlarmEvent(payload)(lines ~610-635): postsalarmFiredon the main handler through a@Volatile activeInstance(set inconfigureFlutterEngine, cleared inonDestroy); no-op with log when engine dead. Reqs: S2-R3. On-device verify. - T-S2a-06
AlarmScheduler.ktsnooze()(lines 266-292): anchor unified tooccurrenceAt + minutesclamped tonow + minutes(postponeNext logic adopted; also persistssnoozeMinutes); returnsNativeSnoozeResultfor the bridge callback. Reqs: S2-R4, Decision 2.2. On-device verify. - T-S2a-07
AlarmScheduler.ktnativeSnoozeStates()(lines 366-385) returns active future snoozes (alarmId + snoozeUntilMillis + snoozeOriginMillis); wired asgetNativeSnoozeStateinMainActivity(line 192). Reqs: S2-R3, Decision 2.1 engine-dead case. On-device verify.
S2a implementation: Dart bridge and state
- T-S2a-08 [GREEN]
EventoAlarmaAndroidextended withsnoozeUntilMillisfield andaccionSnoozedconst;app.dart_abrirAlarmaSonandoignoressnoozedevents (EstadoAlarmas owns them). DONE. - T-S2a-09 [GREEN]
programar()already sentsnoozeUntilMillis/snoozeOriginMillis(pre-existing); now LOCKED by test (servicio_alarmas_snooze_test.dartpayload test). Reqs: S2-R6. No code change needed. - T-S2a-10 [GREEN]
EstadoAlarmassubscribes toandroid.eventosAlarmain the CONSTRUCTOR (notinicializar— see deviations);_alRecibirEventoNativo(estado_alarmas.dart:266) records the snooze viaposponerEjecucionHasta+_aplicar+notifyListeners, with NO secondandroid.programar. Subscription cancelled indispose. Reqs: S2-R3, S2-R2. DONE. - T-S2a-11 [GREEN]
_sincronizarEjecucionesGestionadasPorAndroidnow 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. - T-S2a-12 [GREEN]
obtenerEstadoSnoozeNativo()added toPuertoAlarmasAndroid+ impl invokinggetNativeSnoozeState; newEstadoSnoozeNativomodel withfromMap. DONE. - T-S2a-13 [GREEN] S2-R5 implemented in
servicio_alarmas.dart_recalcular(line 395):snoozeActivonow requiresalarma.activa, so disabling an alarm clears its snooze;finalizarEjecucionalready cleared it viacompletarEjecucionand 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
- 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.
- 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;_detenerrefactored to reuse it. Reqs: S2-R1. DONE. - T-S2a-16 [GREEN] Snooze button row (
_opcionesSnooze()= sorted {3,5,10,custom}) rendered withl10n.alarmSnoozeOptionLabel(min)+l10n.snoozeActionheader, each wired to_posponer. New ARB keys added to ALL 13 locales. Reqs: S2-R1-A/B/C. DONE.
S2a verification
- T-S2a-17
flutter teston the three snooze test files — all green (RED phase captured first: compile failures + anchor mismatch). - T-S2a-18
flutter test(full suite) — 77/77 passing, no regressions. - T-S2a-19
flutter analyze—No issues found!. - T-S2a-20
dart formatapplied to all touched Dart files (lib + test).
S2a Definition of Done
flutter testgreen (new snooze tests passing; 12 existing files unbroken).flutter analyzeclean.dart formatapplied.- 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
- T-S2b-01 [RED]
test/pantallas/pantalla_alarma_sonando_scaffold_test.dart: assertsPluriWaveScaffoldpresent; noScaffoldwithColor(0xFF061722);Animatepresent normally and ABSENT withdisableAnimations=true. Reqs: S2-R7, S5-R3. DONE. - 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.
- Test A: next-trigger preview present (key
S2b implementation
- T-S2b-03 [GREEN] Ringing screen migrated to
PluriWaveScaffold;Color(0xFF061722)removed,Color(0xFFFFB86B)→tokens.warmCoral;blurSigmacapped to 10 with cold-GPU comment (Design 2.4 mitigation). Reqs: S2-R7, S5-R1 (partial). DONE. - T-S2b-04 [GREEN]
lib/tema/pluri_animate.dartcreated:pluriFadeIn/pluriScaleInreturning the child untouched whenMediaQuery.maybeDisableAnimationsOf(context)is true. Reqs: S5-R3. DONE. - T-S2b-05 [GREEN] Glass surface wrapped in
.pluriFadeIn(context)entry animation. Reqs: S2-R7. DONE. - T-S2b-06 [GREEN]
_vistaProximaEjecucionin the editor: computescalcularProximafrom the in-progress draft (respects vacations/exceptions), rendersalarmNextExecution/alarmNoNextExecution, recomputed on everysetStateso it tracks time/recurrence edits live. Reqs: S2-R8. DONE. - T-S2b-07 [GREEN]
DropdownButtonFormFieldreplaced by_CampoSelectorEmisora+_SelectorEmisoraSheet(bottom sheet withSearchBarover favorites + "no station" option); second identical picker added foremisoraFallback(NEW field in the editor).AlarmaMusical.copyWithgainedlimpiarEmisora/limpiarEmisoraFallbackso "none" actually clears. Reqs: S2-R9. DONE. - T-S2b-08 [GREEN] Snooze duration SegmentedButton (3/5/10 + current custom value) writing
_snoozeMinutos(saved viacopyWith(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
- T-S2b-09
flutter teston both S2b test files — 7/7 green (RED captured first). - T-S2b-10
flutter test(full suite) — 77/77 passing, no regressions. - T-S2b-11
flutter analyze—No issues found!. - T-S2b-12
dart formatapplied to all touched files.
S2b Definition of Done
flutter testgreen.flutter analyzeclean.dart formatapplied.- 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: twoServicioAlarmasAndroidinstances do not share_eventosController(S3-R2-A). Use a fakeMethodChannel. DONE — two distinct channels, simulatedalarmFiredviahandlePlatformMessage, both directions asserted. - T-S3a-02 [RED] Create
test/servicios/servicio_alarmas_cache_test.dart:- Test A:
recalcularTodasdoes NOT callSharedPreferences.setStringwhen schedule unchanged (S3-R5-A). - Test B:
recalcularTodascallsSharedPreferences.setStringexactly once when changed (S3-R5-B). - Test C: Two concurrent
guardarAlarmacalls produce consistent final state — no lost write (S3-R7-A, S6-R2 test #1 preview). DONE —_PrefsEspia implements SharedPreferences(counts setString/getString); Test C also asserts the mutations hydrate the cache at most once.
- Test A:
- T-S3a-03 [RED] Create
test/estado/estado_alarmas_ejecuciones_test.dart:_ejecucionesEmitidaswith 100 stale entries triggers pruning; length ≤ max threshold after prune (S3-R6-A). DONE — plus a cap test (250 fresh entries → ≤ 200). - T-S3a-04 [RED] Create
test/widgets/mini_reproductor_configurar_test.dart:configurarLocalizacionescalled at most once per locale change across 10 rebuilds (S3-R3-A). DONE — counter subclass ofEstadoRadio; 10 notifyListeners rebuilds → 1 call; locale es→en → 2nd call.
S3a implementation
- T-S3a-05 [GREEN] Edit
lib/servicios/servicio_alarmas_android.dart(lines 117-120): convertstatic _eventosController,static _handlerInstalado,static _l10nto INSTANCE fields. Install handler in constructor.Add deprecated static shimDEVIATION: Dart forbids a static and an instance member with the same name; the ONLY call site (estado_radio.dart:74) was rewired in this same change, so no shim exists.configurarLocalizacioneswas added to thePuertoAlarmasAndroidINTERFACE;EstadoAlarmas.configurarLocalizacionesforwards to its bridge;app.dartconfigures it once per locale change. Reqs: S3-R2. DONE. - T-S3a-06 [GREEN]
MiniReproductorconverted toStatefulWidget;configurarLocalizacionesmoved todidChangeDependenciesguarded by cachedLocale. Alarm-bridge l10n hoisted toapp.dart_PaginaPrincipalState.didChangeDependencies(design 3.3 alternative), also locale-guarded and placed BEFORE the early-return. Reqs: S3-R3. DONE. - T-S3a-07 [GREEN]
main.dartresolvesSharedPreferences.getInstance()ONCE beforerunApp;PluriWaveApp(prefs:)injects it intoEstadoRadio,EstadoAlarmas(→ defaultServicioAlarmas(prefs:)), andEstadoIdioma. Reqs: S3-R4. DONE. - T-S3a-08 [GREEN] All inline
getInstance()sites migrated to injected-with-fallback_resolverPrefs():estado_radio.dart(10 sites),servicio_ecualizador.dart(6),servicio_grabacion_radio.dart(4),servicio_contenido_app.dart(3).rg 'SharedPreferences.getInstance()'in lib/ now shows ONLY main.dart plus one fallback expression per class. Reqs: S3-R4. DONE. - T-S3a-09 [GREEN] Dirty-guard in
recalcularTodas(servicio_alarmas.dart:189-207): serializes the recalculated config and compares against the cached raw; skips_guardarand returns the loaded config when identical. Reqs: S3-R5. DONE. - T-S3a-10 [GREEN] In-memory
ConfiguracionAlarmas? _cache+_cacheRaw+_enColaFuture-chain writer queue (mirrors_colaCambioFuente). ALL mutations (guardarAlarma,eliminarAlarma,guardarVacaciones,recalcularTodas,sincronizarEjecucionesNativas,saltarProxima,posponerEjecucionHasta,completarEjecucion) run queued over_configActual()(cache-or-hydrate). DEVIATION (intentional): publiccargar()still re-reads from prefs (cache reset inside the queue) becauseEstadoRadio.importarConfigwrites the raw alarms key DIRECTLY to prefs — a fully cachedcargar()would make imports invisible until restart. Reqs: S3-R7. DONE. - T-S3a-11 [GREEN]
_ejecucionesEmitidasbounded:maxEjecucionesEmitidas = 200cap with oldest-millis eviction + 24 h retention prune (_depurarEjecucionesEmitidas), run on every add (_registrarEjecucionEmitida) and at the start of each_vigilarAlarmasVencidaspass.@visibleForTestinglength getter. Reqs: S3-R6. DONE.
S3a verification
- T-S3a-12 Run
flutter teston the four new S3a files — green (RED captured first: 1 passed / 6 failed across the batch). - T-S3a-13 Run
flutter test(full suite) — 89/89 passing (77 baseline + 12 new), no regressions. - T-S3a-14 Run
flutter analyze—No issues found!. - T-S3a-15 Run
dart formaton all edited Dart files (19 files, 5 reflowed).
S3a Definition of Done
flutter testgreen.flutter analyzeclean.dart formatapplied.- 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
_intencionReproducirflag 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/pauseevent sets_intencionReproducirto false and pauses playback. (S3-R1) - Test B: interruption
end/shouldResumeresumes playback. (S3-R1) - Test C: becoming-noisy event pauses playback. (S3-R1)
DONE — 5 tests (also: end without prior interruption-pause does NOT resume; duck begin/end attenuates and restores) against a fake
ObjetivoAudioInterrumpible.
- Test A: interruption
S3b implementation
- T-S3b-02 [GREEN]
lib/servicios/servicio_audio_session.dartcreated:ServicioAudioSessionconfiguresAudioSessionConfiguration.music().copyWith(androidWillPauseWhenDucked: true), subscribes tointerruptionEventStream+becomingNoisyEventStream. Pause-type begin → pause (remembers_pausadoPorInterrupcion); end/pause-type → resume ONLY if we paused; end/unknown → never resume; duck begin/end →setAtenuado(true/false); noisy → hard pause, clears the resume flag. Also defines theObjetivoAudioInterrumpibleinterface (test seam). Reqs: S3-R1. DONE. - T-S3b-03 [GREEN]
PluriWaveAudioHandler implements ObjetivoAudioInterrumpible:_intencionReproducirset true inplay()/playMediaItem()(coversreproducir/reanudar), false inpause()/stop()(coversdetener; interruption pauses route throughpausar()→pause()). Duck =setAtenuadoscaling effective volume by 0.3 (_volumenEfectivo, respected bysetVolumenand_recrearPlayer).ServicioAudioSession.configurar()wired inmain.dartafterregistrarHandler. This is the seam S7 reads. Reqs: S3-R1. DONE.
S3b verification
- T-S3b-04 Run
flutter test test/servicios/servicio_audio_session_test.dart— 5/5 green (RED captured first: load failure, file missing). - T-S3b-05 Run
flutter test(full suite) — 89/89, no regressions. - T-S3b-06 Run
flutter analyze—No issues found!. - T-S3b-07 Run
dart format lib/servicios/servicio_audio_session.dart lib/servicios/servicio_audio.dart— applied (included in the 19-file format pass).
S3b Definition of Done
flutter testgreen.flutter analyzeclean.dart formatapplied.- Reqs checked off: S3-R1 (
flutter analyzeimport 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). DONE (+ cap test with custom maxDelay).
- Test B:
_intencionReproducir=true+ stall →reconectandostate emitted, reconnect scheduled (S7-R2-A, S7-R7). DONE — decision/scheduling tested atControladorReconexionlevel (extracted pure logic; the handler itself needs platform channels); state emission covered by the widget test. - Test C:
_intencionReproducir=false+ stall → NO reconnect (S7-R2-B, S7-R7). DONE. - Test D: after
maxRetriesexhausted → error state emitted (S7-R2-C, S7-R7). DONE (DecisionReconexion.agotado→ handler falls through to existing terminal error path). - Test E: successful reconnect resets retry counter (S7-R7). DONE (
restablecer()+ backoff restarts at base). - Test F: user stop during stall cancels reconnect (S7-R6, S7-R7). DONE (
cancelar()kills the pending timer; fired-after-cancel never retries). 8 tests, RED captured first (load failure: controller file missing).
- T-S7-02 [RED] Add test in
test/servicios/servicio_audio_reconnect_test.dart: buffer config (AndroidLoadControl) applied to player construction (S7-R1). DONE — assertsPluriWaveAudioHandler.configuracionCargaAndroidvalues (15s/50s/2.5s/5s/prioritizeTime); construction wiring not unit-testable without platform channels, verified by code + on-device. - T-S7-03 [RED] Add widget test
test/widgets/reconnect_ui_test.dart: noAlertDialog/SnackBarshown while handler inreconectandostate (S7-R3-A). DONE — also asserts spinner + localized "Reconectando..." label and that the manual-retry button appears ONLY in error state.
S7 implementation
- T-S7-04 [GREEN]
_crearPlayernow passesaudioLoadConfiguration: configuracionCargaAndroid(15s/50s/2.5s/5s,prioritizeTimeOverSizeThresholds: true) — values as namedstatic const(bufferMinimo/bufferMaximo/bufferParaIniciar/bufferTrasRebuffer). API verified against installed just_audio 0.9.46 source: all params exist, no deviation. Reqs: S7-R1. DONE. - T-S7-05 [GREEN]
EstadoReproduccion.reconectandoadded;ServicioAudio.estadoStreammaps it from the handler'sreconectandoflag (after the terminal-error check, before cargando). Reqs: S7-R2, S7-R3. DONE. - T-S7-06 [GREEN] Reconnect state machine: pure backoff/decision logic extracted to NEW
lib/servicios/controlador_reconexion.dart(ControladorReconexion, maxReintentos=5, base=1s, cap=30s, injectable timer factory);_gestionarErrorReproduccionenters it for network-class errors (PlayerException2xxx ORTimeoutExceptionfrom the 12s source guard) when intent=play; retries re-issue the source through the revision-guarded_cambiarFuentequeue; success (ready+playing) resets;pause/stop/playMediaItemcancel/reset; exhaustion falls through to the single terminal error._cambiarFuentecompletes normally when reconnect engaged soEstadoRadio.reproducirdoes NOT snackbar during retries (S7-R3). Reqs: S7-R2. DONE. - T-S7-07 [GREEN]
mini_reproductor.dart(spinner for reconectando +playbackStatusReconnectinglabel in_labelEstado),pantalla_reproductor.dart(_WaveHero+_Controlestreat reconectando as loading, not error),visualizador_audio.dart(reconectando keeps the visualizer active like cargando). NEW l10n keyplaybackStatusReconnectingin ALL 13 .arb locales + gen-l10n. Reqs: S7-R3. DONE. - T-S7-08 [GREEN] Boundary comment added at the
_estadoSublistener inpantalla_alarma_sonando.dart: onlyreproduciendocancels the 12s fallback timer;reconectandodoes NOT count as playing, WAV fallback stays authoritative. (Code already correct by construction — the listener checks== reproduciendo.) Reqs: S7-R4. DONE. - T-S7-09 [GREEN]
ServicioGrabacionRadioerror handling untouched; S7-R5 invariant comment added above_fallar. Reqs: S7-R5. DONE.
S7 verification
- T-S7-10 Run
flutter test test/servicios/servicio_audio_reconnect_test.dart test/widgets/reconnect_ui_test.dart— 10/10 green (RED captured first:+0 -2load failures). - T-S7-11 Run
flutter test(full suite) — 99/99 passing (89 baseline + 10 new), no regressions. - T-S7-12 Run
flutter analyze—No issues found!. - T-S7-13 Run
dart formaton all 9 touched Dart files (incl. new controller, pantalla_reproductor, visualizador, grabacion, both test files; 2 reflowed).
S7 Definition of Done
flutter testgreen (all reconnect tests passing).flutter analyzeclean.dart formatapplied.- 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) DONE — round-trip also locks alarmas raw passthrough + "sin asignar" group never exported; malformed cases: invalid JSON, empty string, non-object JSON.
- T-S4a-02 [RED] Create
test/estado/estado_ecualizador_test.dart:- Test A:
aplicarPresetnotifiesEstadoEcualizadorlisteners. (S4-R1-A) - Test B:
EstadoRadiolisteners are NOT rebuilt on EQ preset change. (S4-R1-A, S4-R5) DONE — Test A viacambiarPreset(the public preset-change API); Test B counts both notifiers onestado.ecualizador.cambiarPreset(radio = 0, eq ≥ 1).
- Test A:
S4a implementation
- T-S4a-03 [GREEN] Create
lib/servicios/servicio_export_import.dart:ServicioExportImportclass withexportar(config) → Stringandimportar(json) → ConfiguracionCompleta?. Move alljsonEncode/jsonDecodebackup/restore logic fromlib/pantallas/pantalla_ajustes.dart(~1391 lines).PantallaAjustesdelegates to this service. Reqs: S4-R4. DONE — service owns the v2 envelope (construirExportacion), pretty-print (exportar) and graceful parse (importar→Map?, null on malformed).dart:convertremoved from pantalla_ajustes; EstadoRadio exposesexportarConfigJson/parsearConfigJson. DEVIATION:Map<String,dynamic>instead of aConfiguracionCompletamodel (see apply-progress). - T-S4a-04 [GREEN] Create
lib/estado/estado_ecualizador.dart:EstadoEcualizador extends ChangeNotifier— owns preset, bands, enabled flag. Move EQ state fields and methods fromlib/estado/estado_radio.dart. AddProxyProviderregistration inMultiProvider(wherever providers are registered, likelylib/main.dartorlib/app.dart). Reqs: S4-R1. DONE — owns principal/actual/per-station presets + activo, persistence via ServicioEcualizador, application via ServicioAudio;emisoraActualUuidcallback decouples it from station lists. Registered viaListenableProvider(not ProxyProvider — see deviation) inapp.dart; instance owned/disposed by EstadoRadio during the S4 transition. - T-S4a-05 [GREEN] Edit
lib/estado/estado_radio.dart: add backward-compatible getters delegating EQ state toEstadoEcualizador(transition bridge). These are removed in S4b. Add// TODO(S4b): remove gettercomments. DONE — 15 delegating getters/methods, every one tagged// TODO(S4b): remove getter. EQ fields,_cargarEcualizadorPersistido,_aplicarPresetActivo,_presetParaEmisoraremoved from EstadoRadio. - T-S4a-06 [GREEN] Edit
lib/widgets/ecualizador_widget.dartandlib/pantallas/pantalla_ajustes.dartEQ sections: consumecontext.watch<EstadoEcualizador>()(scoped). Screens still compile via compat getters if missed. Reqs: S4-R5. DONE —_SeccionEcualizadornowConsumer2<EstadoRadio, EstadoEcualizador>(radio only for station/favorite info);ecualizador_widget.dartis purely presentational (props + callbacks), no change needed. ALSO rewiredpantalla_reproductor.dartEQ toggle (required for correctness — see deviation).
S4a verification
- T-S4a-07 Run
flutter test test/servicios/servicio_export_import_test.dart test/estado/estado_ecualizador_test.dart— 4/4 green (RED captured first:+0 -2load failures). - T-S4a-08 Run
flutter test(full suite) — 103/103 passing (99 baseline + 4 new), no regressions. - T-S4a-09 Run
flutter analyze—No issues found!. - T-S4a-10 Run
dart formaton all 8 touched Dart files (4 reflowed); analyze + full suite re-run after format.
S4a Definition of Done
flutter testgreen.flutter analyzeclean.dart formatapplied.- 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:ServicioGrabacionRadiois managed byEstadoGrabacion; notifies listeners on recording state change. (S4-R2) DONE — 4 tests: notify-on-state-change, iniciar delegates with current station, no-station → alError without service call, service error state → alError. - T-S4b-02 [RED] Create
test/estado/estado_busqueda_test.dart: search query update notifiesEstadoBusquedalisteners. (S4-R3) DONE — 3 tests: notify on buscar, pagination/memory cap (moved from estado_radio_test), identity-stableresultadosgetter (S4-R5 enabler). - T-S4b-03 [RED] Add widget test: changing EQ preset does NOT rebuild
PantallaInicio(S4-R5-A). DONE —test/pantallas/pantalla_inicio_rebuild_test.dartviadebugPrintRebuildDirtyWidgetslog probe (dirty-flag probe is invalid: provider defers dependent notification to the next build phase) + positive control (cargarPopulares DOES rebuild).
S4b implementation
- T-S4b-04 [GREEN] Create
lib/estado/estado_grabacion.dart:EstadoGrabacion extends ChangeNotifier— owns recording state +_escucharGrabacionsubscription (currentlyestado_radio.dart:51, :79). Register inMultiProvider. Reqs: S4-R2. DONE — owns ServicioGrabacionRadio, the state subscription, dir/maxBytes/open-file actions and thepluriwave/file_actionsMethodChannel;emisoraActual+alErrorcallback seams (mirrors S4a). ListenableProvider in app.dart. - T-S4b-05 [GREEN] Create
lib/estado/estado_busqueda.dart:EstadoBusqueda extends ChangeNotifier— owns search query, results, loading state. Register inMultiProvider. Reqs: S4-R3. DONE — also owns nearby-stations (cercanas) lookup and min-bitrate filter (they shared search state);ordenListas/textos/alErrorcallback seams. ListenableProvider in app.dart. - T-S4b-06 [GREEN] Edit
lib/pantallas/pantalla_inicio.dart(line 43): replace rootcontext.watch<EstadoRadio>()withcontext.select/Consumerscoped to fields it actually reads. Reqs: S4-R5. DONE — selects over identity-memoized getters (NEWlib/estado/orden_emisoras.dartMemoLista); cercanas/genre-search sections consume EstadoBusqueda. - T-S4b-07 [GREEN] Edit
lib/pantallas/pantalla_ajustes.dart(~6 watch sites): replace eachcontext.watch<EstadoRadio>()with scopedcontext.select/Consumerfor the specific field. Reqs: S4-R5. DONE — Grabaciones → watch; Timer/Orden/Grupos/Preferida/Emisoras → context.select; _SeccionInfo keeps its scoped Consumer. - T-S4b-08 [GREEN] Edit
lib/pantallas/pantalla_favoritos.dart: scope theEstadoRadiowatch. Reqs: S4-R5. DONE — selects listaFavoritos + gruposFavoritos. ALSO: pantalla_buscar root watch → watch; pantalla_reproductor_GrabacionWidget→ watch (required: EstadoRadio no longer notifies on recording/search). - 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 gettercomments). Reqs: S4-R1, S4-R2, S4-R3. DONE — all 15 compat members removed (zero TODO(S4b) in lib/); recording + search state/methods extracted; EstadoRadio 1121 (pre-split) → 753 lines, focused on playback/stations/favorites orchestration.
S4b verification
- T-S4b-10 Run
flutter test test/estado/estado_grabacion_test.dart test/estado/estado_busqueda_test.dartplus the rebuild scope test — 8/8 green (RED captured first:+0 -3load failures). - T-S4b-11 Run
flutter test(full suite) — 110/110 passing (103 baseline − 1 moved pagination test + 8 new), no regressions. - T-S4b-12 Run
flutter analyze—No issues found!. - T-S4b-13 Run
dart formaton all 15 touched Dart files (10 reflowed); analyze + suite re-run after format.
S4b Definition of Done
flutter testgreen.flutter analyzeclean.dart formatapplied.- 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: favoriteInkWellhas 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:pluriFadeInreturns unanimated child whendisableAnimations=true(S5-R3-A). ~15 lines. - T-S5-03 [RED] Create
test/pantallas/pantalla_alarmas_fecha_test.dart:_fechaCortawith localeen-USreturnsDateFormat.yMd('en-US')result, NOT11/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.notificationColorequals 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/, excludingpantalla_alarma_sonando.dartdone in S2b): replace withPluriWaveTokensorTheme.of(context).colorSchemereferences. Reqs: S5-R1. ~30 lines across files. - T-S5-08 [GREEN] Edit
lib/widgets/tarjeta_emisora.dart(lines 238-289): wrap mini favoriteInkWellinSemantics(button: true, label: l10n.toggleFavorite); setconstraints: BoxConstraints(minWidth: 48, minHeight: 48). AddsemanticLabelto_AssetIcon/alarm PNG. Reqs: S5-R2. ~15 lines. - T-S5-09 [GREEN]
lib/tema/pluri_animate.dartalready 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 withintl.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 usingAppLocalizationsstationCount(n)plural message. Add the ARB plural entry tolib/l10n/*.arbfiles for all supported locales. Reqs: S5-R5. ~20 lines (Dart) + ARB entries. - T-S5-12 [GREEN] Edit
lib/widgets/tarjeta_emisora.dartshimmer placeholders (lines 389-420): applyBorderRadiusmatching card corners. Editlib/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.darticon sites (lines 985, 1028, 1031): replace non-_roundedicon variants with their_roundedequivalents. Reqs: S5-R7. ~5 lines. - T-S5-14 [GREEN] Edit
lib/main.dart(line 23)AudioServiceConfig: setnotificationColortoPluriWaveTokens.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 (noColor(0x...)in modified files beyond token definitions). - T-S5-18 Run
dart formaton all edited files.
S5 Definition of Done
flutter testgreen.flutter analyzeclean (no new color literals in modified files).dart formatapplied.- 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: rapidplayMediaItem(A),playMediaItem(B),playMediaItem(C)— only C's source active; no stale error from A/B (S6-R2 test #3). Use fakeAudioPlayerseam. ~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: underlinter.rulesaddcancel_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()orawait, 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 analyzewith 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 formaton all edited files.
S6 Definition of Done
flutter testgreen — all 5 required tests present and passing; 12 original files unbroken.flutter analyzeclean under hardenedanalysis_options.yaml.dart formatapplied.- 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: setphase: 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.
- Alarm fires app-killed (S1-R1, S1-R2): kill the app; wait for a scheduled alarm; confirm
PluriWaveAlarmServicestarts with noForegroundServiceTypeExceptionin logcat; exactly one notification in the tray. - 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).
- 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. - 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.
- 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.
- Reboot persistence (S1-R1, S2-R4): schedule an alarm; reboot device; confirm alarm still fires at scheduled time.
- Fallback station attempted (S1-R4): set primary station to an invalid URL, set
emisoraFallbackto a valid one; let alarm fire; confirm the fallback station plays (or bundled WAV if fallback also fails). - Battery optimization exemption requested (S1-R5): fresh install; grant alarm permission; confirm the battery-optimization dialog appears exactly once.
- 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.
- Phone call pauses radio (S3-R1): while radio plays, receive a call; confirm radio pauses/ducks; confirm it resumes after the call.
- 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 |