# Exploration: App quality and native alarms Consolidated record of three parallel explorations (alarms/notifications, architecture/robustness, UI/UX) over the PluriWave Flutter app. Findings preserve `file:line` evidence so the proposal and later phases can act without re-discovery. ## Headline The custom native alarm implementation is the RIGHT architecture and must be kept, not replaced. The work is to close reliability gaps (some CRITICAL on Android 14+), reach UX parity with the native Android Clock app, harden runtime robustness, split a god-class, and run a design-system / a11y / i18n / quality-gate pass. No plugin migration. ## 1. Alarms and notifications ### Why the native implementation stays The custom native alarm stack already does the hard, correct things: - Schedules with `setAlarmClock` using `AlarmClockInfo` (survives Doze, shows the system status-bar alarm icon). - Reschedules from `BOOT_COMPLETED`, `LOCKED_BOOT_COMPLETED`, `MY_PACKAGE_REPLACED`, `TIME_SET`, `TIMEZONE_CHANGED` receivers. - Uses device-protected storage so alarms survive before first unlock. - Handles `SCHEDULE_EXACT_ALARM` + `USE_EXACT_ALARM` and the `POST_NOTIFICATIONS` Android 13 flow. - Plays with `USAGE_ALARM` AudioAttributes, holds a `PARTIAL_WAKE_LOCK` (10 min cap), times out the stream at 15s and falls back to a bundled WAV. Plugin alternatives were evaluated and rejected: the `alarm` package, `android_alarm_manager_plus` + `flutter_local_notifications`, and `awesome_notifications` all conflict with the radio-as-alarm audio control model. DECISION: keep native, fix gaps. ### Gaps (with severity) | # | Severity | Gap | Evidence | |---|----------|-----|----------| | A1 | CRITICAL | `foregroundServiceType="mediaPlayback"` is missing the `alarm` type and the `FOREGROUND_SERVICE_ALARM` permission. On API 34+ this throws `ForegroundServiceTypeException` and the alarm fails silently. | `AndroidManifest.xml`, `PluriWaveAlarmService.kt` | | A2 | CRITICAL | Snooze UI is absent on the ringing screen; only "Detener" exists. The native notification already exposes a `Posponer` action, so state diverges from `EstadoAlarmas` until the next sync. | `pantalla_alarma_sonando.dart:168-212` | | A3 | HIGH | Duplicate FSI notifications: `PluriWaveAlarmReceiver.showFireNotification` (id `59*hash+9`) and `PluriWaveAlarmService` (`NOTIFICATION_ID 92841`) both post simultaneously; `dismissFireNotification` only cancels the receiver's. | `PluriWaveAlarmReceiver.kt`, `PluriWaveAlarmService.kt` | | A4 | HIGH | Fire channels `pluriwave_alarm_fire` / `pluriwave_alarm_native` are created `IMPORTANCE_HIGH` without `setSound(uri, alarmAudioAttributes)`. Android 8+ locks channel sound at creation time, so the alarm AudioAttributes never apply. | channel creation in native layer | | A5 | MEDIUM | `emisoraFallback` exists in model/editor/persistence but `ServicioAlarmasAndroid.programar()` never passes it; Kotlin `NativeAlarmSpec` lacks the field. The fallback station is silently ignored natively. | `servicio_alarmas_android.dart`, `NativeAlarmSpec` (Kotlin) | | A6 | MEDIUM | Native path has no fade-in; `fadeInSegundos` is only honored by `PantallaAlarmaSonando._iniciarFadeIn`. When native plays (screen not foregrounded), audio starts at full volume. | `pantalla_alarma_sonando.dart` `_iniciarFadeIn` | | A7 | MEDIUM | `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` is declared but never requested in `_solicitarPermisosNecesariosParaAlarma`. | permission flow | | A8 | LOW | 3 notification channels where 2 would suffice. | native channel setup | ### Inherited risks from `alarm-clock-module` - `apply-progress.md` states `flutter build` was never run, so the Kotlin native layer has never been compiled or verified. Native changes in this change carry compile risk until a build is run by the user. - `state.yaml` says `phase: tasks-ready` while `apply-progress.md` exists -> state drift to reconcile at archive time (LOW). ## 2. Architecture and robustness | # | Severity | Issue | Evidence | |---|----------|-------|----------| | B1 | CRITICAL | `audio_session ^0.1.21` declared in pubspec but ZERO imports in `lib/`. No audio-focus handling: phone calls and other apps do not pause the radio. | `pubspec.yaml:19` | | B2 | CRITICAL | Static broadcast `StreamController` + static `_handlerInstalado` make the service un-testable and globally shared. | `servicio_alarmas_android.dart:117-119` | | B3 | CRITICAL | `configurarLocalizaciones(l10n)` is called inside `build()`, so it fires on every `notifyListeners` including buffer events (dozens of times per second). | `mini_reproductor.dart:23` | | B4 | HIGH | `EstadoRadio` is a 1121-line god-class owning 6 services plus direct I/O. `SharedPreferences.getInstance()` appears at 25+ sites incl. `servicio_ecualizador.dart` and `servicio_grabacion_radio.dart`. | `estado_radio.dart`, multiple services | | B5 | HIGH | `Timer.periodic` 10s vigilance + 60s refresh; `recalcularTodas()` writes SharedPreferences unconditionally every minute. | `estado_alarmas.dart:316-323` | | B6 | HIGH | `cargar()` runs before every mutation, no cache, racy interleaving (read-modify-write N+1). | `servicio_alarmas.dart:81-108` | | B7 | HIGH | `unawaited(radio.reproducir)` swallows errors on the path toward the alarm screen. | `app.dart:324` | | B8 | HIGH | 1391-line settings screen with inline `jsonDecode`/`jsonEncode` backup logic. | `pantalla_ajustes.dart` | | B9 | MEDIUM | `_ejecucionesEmitidas` is an unbounded `Set` (memory growth over time). | `estado_alarmas.dart:32` | | B10 | MEDIUM | Empty `catch(_){}` swallowing errors. | `servicio_audio.dart:343,346,406,428,448`; `servicio_grabacion_radio.dart:156,165,177,288` | | B11 | MEDIUM | Root `context.watch()` forces full-screen rebuilds. | `pantalla_inicio.dart:43`; 6 sites in `pantalla_ajustes` | | B12 | MEDIUM | Bare `flutter_lints`; no robustness lints enabled. | `analysis_options.yaml` | | B13 | MEDIUM | Module-level `_handlerGlobal` with assert-only guard. | `servicio_audio.dart:19-32` | | B14 | LOW | Dead code. | `servicio_timer.dart:82-91` | | B15 | LOW | Typo shim `agregarEmitoraCustom`. | `estado_radio.dart:887` | | B16 | LOW | Hardcoded version list. | `servicio_contenido_app.dart:32` | ### Test base 12 test files exist (good base). Top-5 missing tests: `ServicioAlarmas` concurrent read-modify-write, alarm fire dedup across `refrescarProgramacion`, `PluriWaveAudioHandler` rapid source-switch race, export/import round-trip, `ServicioGrabacionRadio` error recovery. ## 3. UI / UX Foundation is solid: Material 3 + `ThemeExtension` tokens (`PluriWaveTokens` / `PluriWaveMotion`) + `PluriGlassSurface`. | # | Severity | Issue | Evidence | |---|----------|-------|----------| | C1 | HIGH | Ringing screen is a raw `Scaffold` with `Color(0xFF061722)`, a static PNG, no animation, no snooze. | `pantalla_alarma_sonando.dart:155-212` | | C2 | HIGH | No `Hero` between `TarjetaEmisora` logo and player `_WaveHero`; custom `PageRouteBuilder` needs `HeroFlightShuttleBuilder` care with `BackdropFilter`. | `tarjeta_emisora.dart`, player route | | C3 | HIGH | 14+ hardcoded `Color(0x...)` literals. | `pantalla_alarmas.dart:94,144,775`; `pluri_wave_scaffold.dart:34-37,48,53`; `pantalla_alarma_sonando.dart:159,167`; `pluri_premium_widgets.dart:41,185`; `tarjeta_emisora.dart:191` | | C4 | HIGH | Mini favorite `InkWell` 36x36 has no `Semantics` and is below the 48dp target. | `tarjeta_emisora.dart:238-289` | | C5 | HIGH | Alarm PNG has no `semanticLabel`. | alarm image widgets | | C6 | MEDIUM | No reduced-motion handling anywhere (`MediaQuery.disableAnimations` ignored). | app-wide | | C7 | MEDIUM | Alarm editor: no next-trigger preview, dropdown station picker, no section structure, keyboard-overflow risk in sheet. | `pantalla_alarmas.dart:387-636` | | C8 | LOW-MED | `_fechaCorta` hardcodes DD/MM/YYYY, breaking ja / en-US / ar locales. | `pantalla_alarmas.dart:1114` | | C9 | LOW | Bare counters without pluralization. | `pantalla_favoritos.dart:138` | | C10 | LOW | Shimmer sharp corners. | `tarjeta_emisora.dart:389-420` | | C11 | LOW | Buscar loading spinner inconsistent with shimmer pattern. | `pantalla_buscar.dart:241-245` | | C12 | LOW | Non-rounded icon variants. | `pantalla_ajustes.dart:985,1028,1031` | | C13 | LOW | `notificationColor` is the M3 default purple, not brand. | `main.dart:23` | Dark-only theme: light mode is explicitly OUT OF SCOPE unless requested. ## Recommendation Proceed to proposal. Ship reliability first (Slice 1), then UX parity (Slice 2), then runtime robustness (Slice 3), then the `EstadoRadio` split (Slice 4), then design/a11y/i18n (Slice 5), then quality gates and tests (Slice 6). Each slice is a chained, PR-sized unit under 400 changed lines. Strict TDD applies via `flutter test`. The user must run `flutter build` to validate the native (Kotlin) layer, since it has never been compiled. ## Ready for Proposal Yes.