Files
pluriwave/openspec/changes/app-quality-and-native-alarms/explore.md
T
FreeTLab f3e9487215 feat(alarms): native reliability fixes and end-to-end snooze
- 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)
2026-06-11 15:33:30 +02:00

8.7 KiB

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<EstadoRadio>() 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.