Close the SDD cycle: verified pass with warnings (0 critical), slice 1 cancelled with SDK evidence, slices 2-3 shipped, post-verify dispose fix landed. The native-alarms capability spec is promoted to openspec/specs/ with the corrected FGS requirement. Phase 5 on-device QA remains the pending human gate.
28 KiB
Tasks: Alarm Volume Ramp & Device-Volume Immunity
Review Workload Forecast
| Field | Value |
|---|---|
| Estimated changed lines | 260-360 (7 files: manifest, 2 Kotlin, 3 Dart, 1 new Dart interface method pair + 3 test files) |
| 400-line budget risk | Medium |
| Chained PRs recommended | Yes |
| Suggested split | PR 1 (Slice 1 manifest+FGS) -> PR 2 (Slice 2 volume override/restore) -> PR 3 (Slice 3 fade-in dedup) |
| Delivery strategy | ask-on-risk |
| Chain strategy | pending |
Decision needed before apply: Yes Chained PRs recommended: Yes Chain strategy: pending 400-line budget risk: Medium
Suggested Work Units
| Unit | Goal | Likely PR | Notes |
|---|---|---|---|
| 1 | Fix FGS manifest+runtime type mismatch (Slice 1) | PR 1 | Independent, near-zero risk, own rollback; mandatory Android 14+ device gate |
| 2 | Ring-scoped STREAM_MUSIC override + idempotent restore (Slice 2) | PR 2 | Depends on PR 1 landing (FGS must start before override matters); own rollback via guard-disable |
| 3 | Dedup fade-in driver at handoff (Slice 3) | PR 3 | Independent of PR 2; can reorder before PR 2 if preferred; own rollback (revert gate) |
Phase 1: FGS Manifest + Runtime Type Fix (Slice 1 — code-inspection + manual QA)
PHASE CANCELLED (orchestrator, 2026-07-11) — resolution of the BLOCKED note below: the target constants are fictional (verified against the local SDK), the existing
mediaPlayback|systemExempteddeclaration is confirmed correct, and Root Cause B is withdrawn. Tasks 1.1-1.7 ship no code. See the correction banner in design.md and the amended Requirement in specs/native-alarms/spec.md. Slices 2 and 3 proceed unaffected.
- 1.1 Edit
android/app/src/main/AndroidManifest.xml:57— changePluriWaveAlarmServiceandroid:foregroundServiceTypefrom"mediaPlayback|systemExempted"to"mediaPlayback|alarm". - 1.2 Edit
android/app/src/main/AndroidManifest.xml:6— replace<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/>with<uses-permission android:name="android.permission.FOREGROUND_SERVICE_ALARM"/>. - 1.3 Edit
PluriWaveAlarmService.kt:118-119— changestartForegroundtype constants fromFOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTEDtoFOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or FOREGROUND_SERVICE_TYPE_ALARM. 1.1-1.3 are ONE atomic unit — a manifest-only or runtime-only edit leaves the API 34+ exception alive; do not split across commits.
BLOCKED (sdd-apply, 2026-07-11):
FOREGROUND_SERVICE_TYPE_ALARM/android.permission.FOREGROUND_SERVICE_ALARMdo NOT exist in the Android SDK. Verified viajavap -constantsagainstandroid.content.pm.ServiceInfoandandroid.Manifest$permissionin the localandroid-34/android-35/android-36platformandroid.jars, plus a fullFOREGROUND_SERVICE_TYPE_*field sweep ofapi-versions.xml. The only FGS types that exist through API 36 are: camera, connectedDevice, dataSync, health, location, manifest, mediaPlayback, mediaProcessing, mediaProjection, microphone, phoneCall, remoteMessaging, shortService, specialUse, systemExempted — noalarmvariant. Edits 1.1-1.3 were applied then reverted (git checkout --) to avoid landing an unresolved-Kotlin-reference / invalid-manifest-enum compile break thatflutter analyzecannot catch (Dart-only) and that this task explicitly forbids validating viaflutter build/gradle. This reconfirms the identical finding already recorded inapp-quality-and-native-alarms(T-S1-03/T-S1-04), which usedsystemExempted/FOREGROUND_SERVICE_SYSTEM_EXEMPTEDfor the same reason — see design #2310's own "Open Questions" section, which flagged but did not resolve this before approval. Design #2310 / tasks #2316 need correction before Slice 1 can proceed: either identify a real SDK-backed fix for the API 34+ForegroundServiceTypeException, or confirmsystemExemptedwas already correct and the actual Slice 1 defect (if any) lies elsewhere. Working tree is clean — no diff left on either file.
- 1.4 Static check:
rg 'foregroundServiceType' android/app/src/main/AndroidManifest.xmlshowsalarm, notsystemExempted, on thePluriWaveAlarmServiceline. - 1.5 Static check:
rg 'FOREGROUND_SERVICE_ALARM|FOREGROUND_SERVICE_SYSTEM_EXEMPTED' android/app/src/main/AndroidManifest.xmlshowsFOREGROUND_SERVICE_ALARMpresent andFOREGROUND_SERVICE_SYSTEM_EXEMPTEDabsent. - 1.6 Static check:
rg 'FOREGROUND_SERVICE_TYPE_ALARM|FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED' android/.../PluriWaveAlarmService.ktshowsTYPE_ALARMpresent andTYPE_SYSTEM_EXEMPTEDabsent — confirms manifest/runtime match (Requirement: Manifest declares alarm-eligible FGS, Scenario "Manifest declares required FGS type and permission"). - 1.7
flutter analyze(noflutter build) — confirm no lint regressions from these edits (none expected; XML/Kotlin only).
Phase 2: Ring-Scoped Volume Override — Kotlin Channel Methods (Slice 2, code-inspection only)
-
2.1 In
MainActivity.kt, add@Volatilecompanion-scoped state:mediaVolumeOverridden: BooleanandcapturedMediaVolume: Int?to track ring-scoped override without surviving process death (documented residual gap). -
2.2 In
MainActivity.kt'salarm_schedulerwhen (call.method)block (near L79-218), add"overrideMediaVolumeForRing"case: capture currentAudioManager.STREAM_MUSICvolume intocapturedMediaVolume(only if not already overridden — idempotent guard), thensetStreamVolume(STREAM_MUSIC, getStreamMaxVolume(STREAM_MUSIC), 0)(flag0= noFLAG_SHOW_UI, no slider flash). SetmediaVolumeOverridden = true.fractionarg accepted but unused (reserved, default1.0= max reference level, per design). -
2.3 In the same
whenblock, add"restoreMediaVolume"case: no-op ifmediaVolumeOverridden == false(idempotent guard); otherwisesetStreamVolume(STREAM_MUSIC, capturedMediaVolume, 0), then clearmediaVolumeOverridden = falseandcapturedMediaVolume = null. -
2.4 Add a public
restoreMediaVolumeBestEffort()method onMainActivity(or companion) thatPluriWaveAlarmServicecan call as a backstop when the engine is alive. -
2.5 In
PluriWaveAlarmService.kt'sstopAlarm()(L356-381) andonDestroy()(L501-504), call the best-effort restore before/alongside existing teardown, guarded so it never throws if the engine/activity is unavailable.RISK NOTE (sdd-apply, 2026-07-11):
stopAlarm()also fires at the native-to-Flutter handoff (confirmFlutterAudiochannel case ->PluriWaveAlarmService.stop()->stopAlarm()), not only at a true dismiss/snooze ring exit —stopAlarm()'s caller has no way to distinguish "handoff" from "real exit" (bothstopNativeAlarmSoundandconfirmFlutterAudiocall the identicalPluriWaveAlarmService.stop(this, id)). Implemented exactly as specified (design #2310 + this task both call for wiring bothstopAlarm()/onDestroy()), but this means the best-effort restore backstop COULD fire mid-ring at the handoff moment, restoring the original (possibly zero) device volume right as the Flutter player takes over — which would silence the Flutter-driven remainder of the ring and contradict Scenario "Alarm is audible when device media volume is 0". Not verifiable without an emulator (Kotlin is code-inspection-only,flutter build/gradle forbidden this phase). Phase 5 QA 5.1 is the exact scenario that will surface this if it manifests — treat as the primary manual QA risk for this change, and flag to the human/design owner before merge.FIX (sdd-apply, 2026-07-11): Risk #1 resolved. Added a
@Volatilecompanion flagPluriWaveAlarmService.flutterOwnsRing(defaultfalse, declared alongside the service's other companion constants).MainActivity.kt'sconfirmFlutterAudiohandler sets it totrueimmediately BEFORE callingPluriWaveAlarmService.stop(this, id)(the handoff trigger) — the flag is now visible by the time the resultingACTION_STOPintent reachesstopAlarm().stopAlarm()andonDestroy()now wrap therestoreMediaVolumeBestEffort()call inif (!flutterOwnsRing), so the backstop no longer fires at the handoff moment — restore ownership passes cleanly to Dart's_silenciarAudio()/dispose()path (already wired, Phase 3) for the remainder of the ring. Native-only exits (fire-notification STOP, real snooze, teardown before any handoff) are unaffected —flutterOwnsRingstaysfalsethere, so the backstop still fires exactly as before. The flag resets tofalseat the top ofstartAlarm()(immediately after theactiveAlarmIdre-entrancy guard passes) so a staletrueleft over from a PREVIOUS ring's handoff can never suppress the CURRENT ring's backstop. New accepted residual, documented inline on the flag: if the Flutter process dies AFTER handoff (flag alreadytrue) but BEFORE Dart's own restore runs, no restorer fires — same class of gap as the pre-handoff process-death residual already documented onrestoreMediaVolumeBestEffort(). Verified viarg 'flutterOwnsRing'(flag declared inPluriWaveAlarmService's companion; set inMainActivity.kt'sconfirmFlutterAudiocase; read-guarded in bothstopAlarm()andonDestroy(); reset instartAlarm()) andflutter analyze(0 issues, Dart untouched, noflutter build/gradle run). OnlyPluriWaveAlarmService.ktandMainActivity.ktchanged for this fix. Phase 5 QA 5.1 remains the recommended on-device confirmation — code inspection cannot fully substitute for a real handoff-timing test. -
2.6 Static check:
rg 'overrideMediaVolumeForRing|restoreMediaVolume' android/.../MainActivity.ktshows both channel cases present. -
2.7 Static check:
rg 'mediaVolumeOverridden' android/.../MainActivity.ktshows the guard read in BOTH the override and restore branches (idempotence, Requirement: Ring-scoped device-volume override, Scenario "Restore is idempotent across double-exit paths"). -
2.8 Static check:
rg 'restoreMediaVolumeBestEffort' android/.../PluriWaveAlarmService.ktshows it called from bothstopAlarmandonDestroy. -
2.9
flutter analyze— confirm no Kotlin/lint regressions. (0 issues.)
Phase 3: Ring-Scoped Volume Override — Dart Port + Wiring (Slice 2, strict TDD)
-
3.1 (RED) In
test/servicios/servicio_alarmas_android_test.dart, add a test assertingServicioAlarmasAndroid.forzarVolumenMediaParaAlarma(1.0)invokes channel methodoverrideMediaVolumeForRingwith{'fraction': 1.0}, using the existing mock-channel pattern (MethodChannel('pluriwave/alarm_scheduler')+llamadaslist). Runflutter test— confirm it fails (method does not exist). -
3.2 (RED) In the same file, add a test asserting
ServicioAlarmasAndroid.restaurarVolumenMedia()invokes channel methodrestoreMediaVolumewith no args. Runflutter test— confirm it fails. -
3.3 (GREEN) Add
Future<void> forzarVolumenMediaParaAlarma(double fraccion)andFuture<void> restaurarVolumenMedia()toPuertoAlarmasAndroid(abstract,lib/servicios/servicio_alarmas_android.dart) and implement both onServicioAlarmasAndroidusing the existing_logAndInvokeVoidhelper pattern. Runflutter test— confirm 3.1-3.2 pass. -
3.4 (GREEN) Extend
test/helpers/fakes_alarmas.dart'sFakePuertoAlarmasAndroid: implement the two new abstract methods, recording calls into new listsvolumenForzado: List<double>andvolumenRestaurado: int(call count) so widget tests can assert invocation order/count. -
3.5 (RED) In
test/pantallas/pantalla_alarma_sonando_dismiss_guard_test.dart(or a new focused test file), add a widget test asserting_silenciarAudio-> restore is called exactly once on_detener()(dismiss) usingenv.android.volumenRestaurado. Runflutter test— confirm it fails. -
3.6 (RED) Add the equivalent test for
_posponer()(snooze) — restore called exactly once. Runflutter test— confirm it fails. -
3.7 (RED) Add a test asserting restore is called at most once total even when both
_silenciarAudio()(inside_detener) anddispose()run in sequence (idempotence at the Dart call-site level — the widget always calls restore indispose()too, per design; assert the FAKE'S restore counter, not double-invocation of the real guard, since idempotence itself lives in Kotlin). Runflutter test— confirm it fails. -
3.8 (GREEN) In
lib/pantallas/pantalla_alarma_sonando.dart, callcontext.read<EstadoAlarmas>().android.restaurarVolumenMedia()inside_silenciarAudio()(L202-213, alongside_liberarAudioLocal()/radio.audio.pausar(), wrapped in its own try/catch so a failure never blocks dismiss/snooze) AND insidedispose()(L238-244). Runflutter test— confirm 3.5-3.7 pass.Implementation note: added a Dart-side
_volumenMediaRestauradoguard (mirroring the existing_audioFlutterConfirmadoidiom) so the channel call fires at most once per screen instance regardless of which of the two call sites runs first — this is what makes 3.7's "at most once" assertion literally true at the Dart layer, on top of the Kotlin-side idempotent guard. -
3.9 (RED) In
test/pantallas(widget test, or a lighter unit-style test onapp.dart's ring-start seam if testable in isolation), add a test assertingforzarVolumenMediaParaAlarmais invoked when an alarm ring starts, at the TOP of_prearrancarAudioAlarmainlib/app.dart(BEFORE theif (emisora == null) return;early exit at L367) — the override must apply even when the alarm uses the fallback WAV path, not only the station path. Runflutter test— confirm it fails.DEVIATION (sdd-apply, 2026-07-11): not testable in isolation, so no
flutter testRED/GREEN pair exists for this task — confirmed via the escape hatch this task's own wording allows._prearrancarAudioAlarmaand_PaginaPrincipalare private toapp.dart; the only public entry point (PluriWaveApp) hardcodes real, non-injectableEstadoRadio/EstadoAlarmasinstances (ServicioDispositivoAudioReal(),EstadoAlarmas(prefs: prefs)— no fake-injection seam), and no existing test in the suite rendersPluriWaveApp/_PaginaPrincipalfor this exact reason (every alarm widget test bypasses it, constructingEstadoRadio/EstadoAlarmasdirectly with fakes). Building a real widget test would require an unscoped DI refactor toapp.dart's constructor, which is not listed in design #2310's File Changes (only "call override in_prearrancarAudioAlarma" — a one-line-style modify). Verified via source inspection instead:forzarVolumenMediaParaAlarma(1.0)is confirmed the first statement inside_prearrancarAudioAlarma, beforefinal emisora = alarma.emisora;and the early return (seelib/app.dart:365-374). Recommend a follow-up task if genuine automated coverage of this exact seam is required (would need a testable DI seam onPluriWaveApp). -
3.10 (GREEN) In
lib/app.dart, callcontext.read<EstadoAlarmas>().android.forzarVolumenMediaParaAlarma(1.0)as the FIRST statement inside_prearrancarAudioAlarma(L365), before theemisora == nullearly return. Runflutter test— confirm 3.9 passes.Implementation note: this introduced an
awaitbefore the pre-existingcontext.read<EstadoRadio>()a few lines below, whichflutter analyzecorrectly flagged asuse_build_context_synchronously. Fixed withif (!mounted) return;right after the new await, matching the same guard pattern already used elsewhere in this file (e.g._abrirAlarmaSonando) — not just a lint silencer, this also prevents reading providers / starting playback on an unmounted widget if the app is backgrounded mid-call. -
3.11 (RED) Add a test asserting the override/restore channel methods are NEVER invoked during normal radio playback with no alarm ringing (Requirement: Ring-scoped device-volume override, Scenario "Normal radio playback never triggers the override") — assert
env.android.volumenForzadostays empty across a plain play/pause cycle onEstadoRadiooutside any alarm flow. Runflutter test— confirm it fails or passes vacuously (should already pass since no other code path calls these methods yet — treat as a REGRESSION GUARD, not a RED/GREEN pair, if 3.3-3.10 are already in place). (Confirmed green on first run, as anticipated — regression guard, not RED/GREEN.) -
3.12 (REFACTOR) Run
flutter testfor the full suite plusflutter analyze— confirm no regressions in existing alarm/radio tests. (Full suite: 1 unrelated failure inservicio_grabacion_radio_test.dart— timing-sensitive, reproduces only under full-suite load, passes standalone; confirmed pre-existing, not a Slice 2 regression. All alarm/radio/Slice-2 suites green;flutter analyze0 issues.)
Phase 4: Fade-In Dedup at Handoff (Slice 3, strict TDD)
-
4.1 (RED) In
test/pantallas/pantalla_alarma_sonando_test.dart(or a new focused fade-in test file), add a widget test asserting the Dart fade-in ramp (observable viaFakeServicioAudio/fallback player volume changes) does NOT start beforeconfirmarAudioFlutterhas been invoked on the android port (i.e., before_confirmarAudioFlutterListo()runs) — assertenv.android.detenidas(whichconfirmarAudioFlutterappends to, perFakePuertoAlarmasAndroid.confirmarAudioFlutter) is non-empty before any volume-ramp step is observed. Runflutter test— confirm it fails (current code starts the ramp at L66 immediately afterradio.reproducir, before confirmation). -
4.2 (RED) Add a test asserting the fade-in DOES start once
_confirmarAudioFlutterListo()has run (radio path viaestadoStreamemittingreproduciendo, or fallback path via_iniciarFallback) — the ramp must still function end-to-end after the gate. Runflutter test— confirm it fails or is trivially satisfied depending on 4.1's fixture; treat 4.1+4.2 as one RED pair validating gate correctness both ways.Implementation note: 4.1+4.2 were implemented as a single RED pair (one
testWidgets, two assertion stages) intest/pantallas/pantalla_alarma_sonando_test.dart, radio path. Rather than relying on realestadoStreamsubscription timing (existing_montarPantallapre-emitsreproduciendoBEFORE mount in all prior tests, which — traced via code inspection — means_confirmarAudioFlutterListo()never actually fires in those fixtures: the broadcast stream drops the pre-mount event, and the fallback timer's immediate-cancel branch also short-circuits before any listener would see it), added a new opt-out paramaudioYaReproduciendo(defaulttrue, preserves all existing tests byte-for-byte) plus a test-onlyCompleter-based gate (FakePuertoAlarmasAndroid.puertaConfirmarAudioFlutter) onconfirmarAudioFlutter, so the test can deterministically observe the pre-confirm state, release the gate, then observe the post-confirm state — independent of stream/timer race conditions. Confirmed RED against the pre-4.3 code: the gate-pending assertion failed withActual: [0.05, 0.85](ramp already fired before the gate was ever released), proving the ramp was ungated.Also added a THIRD test beyond the literal 4.1/4.2 wording, covering the explicit edge case in this batch's own dispatch instructions: if
confirmarAudioFlutterFAILS (dead/never-there native channel), the Dart ramp must still start — the ring must never stay stuck at_volumenInicialFadeInforever, since Dart is the only audible source once native is gone. AddedFakePuertoAlarmasAndroid.fallaConfirmarAudioFlutterto simulate this. Confirmed RED against pre-4.3 code too: the fake'sStateErrorpropagated UNCAUGHT out of_confirmarAudioFlutterListo()(no try/catch existed), crashing the test — proving the failure path was unhandled before this slice. -
4.3 (GREEN) In
lib/pantallas/pantalla_alarma_sonando.dart, remove the_iniciarFadeIn()call at L66 (radio path, currently fires immediately afterradio.reproducir(emisora)) and the one at L96 (fallback path, currently fires before_confirmarAudioFlutterListo()at L97); move the single_iniciarFadeIn()invocation INTO_confirmarAudioFlutterListo()(L133-139) so it fires exactly once, after the_audioFlutterConfirmadoguard, for both the radio and fallback paths. Runflutter test— confirm 4.1-4.2 pass.Implementation note: moved as specified.
_confirmarAudioFlutterListo()now wraps the native channel call intry { await ... } catch (e) { debugPrint(...); } finally { _iniciarFadeIn(); }— matching the existingtry/catch/finallyidiom already used in_detener()/_posponer()in the same file. Thefinallyblock is what satisfies the failure-edge case above: whether the native confirmation succeeds or throws, the fade-in always starts exactly once (still gated by the pre-existing_audioFlutterConfirmadoguard at the top of the method, unchanged). All 3 new tests (4.1+4.2 combined, plus the failure-edge test) pass after this change;rg '_iniciarFadeIn' lib/confirms exactly one call site remains (inside_confirmarAudioFlutterListo) plus the function definition itself. -
4.4 (REFACTOR) Re-run the full
pantalla_alarma_sonando_test.dartandpantalla_alarma_sonando_dismiss_guard_test.dartsuites — confirm no existing fade/dismiss/snooze assertions regressed from moving the ramp start point. (Also includedpantalla_alarma_sonando_scaffold_test.dartandservicio_alarmas_android_test.dartfor extra confidence since a shared fake was extended. 22/22 tests green, no regressions — none of the pre-existing tests assert on fade/volume timing, since none of them previously exercised_confirmarAudioFlutterListo()at all under the old pre-mount-emit fixture pattern, as traced in the 4.1/4.2 note above.) -
4.5
flutter analyzeanddart format .— confirm clean formatting/lint state for all Slice 3 edits. (flutter analyze: 0 issues.dart format .reformatted the 3 Slice 3 files cleanly; it also touched 8 files unrelated to this change — pre-existing formatter-version drift onmain, confirmed reproducible and out of scope per this batch's file-scope constraint — reverted viagit checkout --both times it recurred, see Phase 6 6.3 note.)
Phase 5: Manual/On-Device QA (mandatory human gate — Android 14+ physical or emulator device)
- 5.1 Set device media (
STREAM_MUSIC) volume to 0. Trigger an alarm. Confirm the alarm rings audibly for its full duration, not just the pre-handoff native window (Scenario "Alarm is audible when device media volume is 0"). - 5.2 Confirm
PluriWaveAlarmServicestarts withoutForegroundServiceTypeExceptionon an Android 14+ (API 34+) device when the alarm fires from a background/killed-app broadcast context (Scenario "Native service starts from broadcast context on Android 14+"). - 5.3 Note the original device media volume before triggering the alarm; dismiss the alarm; confirm media volume is restored to exactly the pre-alarm value (Scenario "Dismiss restores the original captured volume").
- 5.4 Repeat 5.3 for snooze instead of dismiss (Scenario "Snooze restores the original captured volume").
- 5.5 Listen across the native-to-Flutter handoff window; confirm there is no audible double-ramp/volume jump at the moment
confirmarAudioFlutterfires (Scenario "No double-ramp interleaving at handoff"). - 5.6 With no alarm ringing, play/pause normal radio repeatedly and confirm device volume controls behave exactly as before this change (no override side effects) — Scenario "Normal radio playback never triggers the override".
- 5.7 Optional/best-effort: force-kill the app mid-ring and confirm the residual-volume-override gap is no worse than documented (Scenario "App killed mid-ring — best-effort restore only"; known accepted gap, not a blocking QA failure).
- 5.8 Record QA sign-off (device model, Android version, pass/fail per scenario) before
sdd-verify/merge.
Phase 6: Final Static Sweep
-
6.1 Full-repo
rg 'systemExempted|SYSTEM_EXEMPTED'acrossandroid/app/src/main— confirm zero remaining references (fully dropped, not kept alongsidealarm).INTERPRETATION FLIP (sdd-apply, 2026-07-11): this check's original "zero remaining references" wording was written under the Phase 1 premise that later proved false. Ran it anyway: 3 references found (
AndroidManifest.xml:6permission,AndroidManifest.xml:58foregroundServiceType,PluriWaveAlarmService.kt:124runtime constant) — allsystemExempted/SYSTEM_EXEMPTED, nonealarm. This is the CORRECT and EXPECTED state, not a failure: per the Phase 1 cancellation banner (design.md correction, confirmed viajavap -constantsSDK inspection),FOREGROUND_SERVICE_TYPE_ALARM/FOREGROUND_SERVICE_ALARMare fictional andmediaPlayback|systemExemptedis confirmed the correct, intentional declaration — so "fully dropped" is no longer the right target; "present and internally consistent between manifest and runtime" is. Manifest (systemExemptedat L6+L58) and Kotlin runtime (FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTEDat L124) match each other exactly, same as before this entire change — Slice 1 shipped no code (cancelled), so this file pair was never touched by any batch of this change. Marking complete because the check RAN and its result was interpreted correctly against the corrected design, not because the literal original wording's outcome was achieved. -
6.2
flutter test(full suite) andflutter analyze— final clean run before requesting review.Implementation note: the full-suite invocation reproduced the documented intermittent hang (this task's own dispatch instructions named
estado_alarmas_ejecuciones_test.dartandservicio_grabacion_radio_test.dartas known culprits) — it stalled past+276tests for several minutes of real time with zero further progress and had to be killed. Fell back exactly as instructed: ran all 44 other test files (everything undertest/except those two) together —01:02 +272: All tests passed!, zero failures. Then ran the two known-flaky files standalone —+7: All tests passed!, zero failures, confirming (again) they only misbehave under full-suite load, not on their own — same conclusion already recorded in Batch 2's apply-progress forservicio_grabacion_radio_test.dart(_fallar must clear activa flag immediately, timing- sensitive). 272 + 7 = 279 tests total across all 46 files in the suite, all green.flutter analyze: 0 issues. -
6.3
dart format .— confirm no formatting diffs remain uncommitted.Implementation note:
dart format .left the 3 files this batch touched (lib/pantallas/pantalla_alarma_sonando.dart,test/helpers/fakes_alarmas.dart,test/pantallas/pantalla_alarma_sonando_test.dart) untouched on its second run (already clean from the 4.5 pass) — confirmed viagit status --shortshowing only those 3 files modified. Both timesdart format .ran in this batch (4.5 and 6.3) it ALSO reformatted 8 files this batch never edited (lib/pantallas/pantalla_ajustes.dart,lib/servicios/servicio_ecualizador.dart, and 6 test files undertest/estado,test/pantallas,test/servicios) — reverted both times viagit checkout --to stay within this batch's file-scope constraint (onlypantalla_alarma_sonando.dart+ test files). This is pre-existing formatter-version drift already present onmainbefore this change started, unrelated to Slice 2 or Slice 3 — worth a follow-updart format .cleanup commit on its own, out of scope here.