# Design: EQ Device Auto-Switch UX ## Technical Approach Fix the startup device-seeding bug by adding a single `obtenerDispositivoActual()` call inside `cargarPersistido()` after the subscription is wired. Add a `nombresDispositivos` persistence layer (JSON map in SharedPreferences) exposed through `EstadoEcualizador`. Rewrite `_SeccionEcualizadorAvanzado` device rows with connection dot, display name fallback chain, and tap-to-edit bottom sheet containing rename TextField + embedded `EcualizadorWidget`. ## Architecture Decisions ### Decision: Bug fix location | Option | Tradeoff | Decision | |--------|----------|----------| | A. Call `obtenerDispositivoActual()` in `cargarPersistido()` after `_configurarSuscripcionDispositivo()` | Minimal change, Dart-only, no platform touch | **Chosen** | | B. Self-initializing stream (emit current device on `onListen` from Kotlin) | Single code path for all events | Rejected: requires Kotlin change, out of scope | | C. Persist last-known device ID to SP | Survives process kill | Rejected: stale-state risk, higher complexity | **Rationale**: The `obtenerDispositivoActual()` method channel already exists and is tested on the platform side. One `await` call seeds `_dispositivoActualId`, triggers first-seen bootstrap, and re-resolves the preset. Zero platform code changes. ### Decision: Device names persistence location | Option | Tradeoff | Decision | |--------|----------|----------| | A. New SP key `eq_nombres_dispositivos_v1` in `ServicioEcualizador` as `Map` JSON | Reuses existing `_leerMapa`/`_guardarMapa` pattern (adapted for String values), co-located with other EQ SP keys | **Chosen** | | B. Field on `DispositivoAudio` model | Requires model change + breaks value-equality contract (id-only) | Rejected | | C. Separate `ServicioNombresDispositivo` | Over-engineered for a single map | Rejected | **Rationale**: `ServicioEcualizador` already manages 6 SP keys with the same read/write JSON pattern. Adding a 7th key with a simpler `Map` type keeps all EQ persistence in one service. The `DispositivoAudio` model stays pure (id-based equality, no persistence coupling). ### Decision: Modal architecture | Option | Tradeoff | Decision | |--------|----------|----------| | A. `showModalBottomSheet` with rename + embedded `EcualizadorWidget` | Established pattern (used 4x in settings), `isScrollControlled: true` handles tall content | **Chosen** | | B. Inline expanded accordion per device | Always visible, but very tall list, complex expand/collapse state | Rejected | | C. Full-screen dialog / page route | Overkill for rename + 5-band slider | Rejected | **Rationale**: The app already uses `showModalBottomSheet` with `showDragHandle: true` and `isScrollControlled: true` in `_editarGrupo`, `_editarTamanoMaximo`, `_FormularioDuracionTimer`, and `_mostrarFormularioAnadir`. Same pattern, consistent UX. ### Decision: Display name resolution | Option | Tradeoff | Decision | |--------|----------|----------| | A. Fallback chain: `nombresDispositivos[id]` > `DispositivoAudio.nombre` > `id` | Clean, handles all null/empty cases, no model changes | **Chosen** | | B. Merge custom name into a new model field | Couples persistence to model | Rejected | **Rationale**: The display name is a pure UI concern. `DispositivoAudio.nombre` comes from platform (`productName`) and can be null/empty on some devices. The fallback chain is computed at render time in the settings widget, keeping the model and state layers clean. ### Decision: Connection status derivation **Choice**: Compare `entry.key == eq.dispositivoActualId` per row to show a green dot. No new API needed -- `EstadoEcualizador.dispositivoActualId` getter already exists. **Rejected**: Tracking a `Set connectedDevices` from the stream -- unnecessary since only one audio output is active at a time on Android. ## Data Flow ``` App startup | v cargarPersistido() |-- servicio.cargar() --> loads config + nombresDispositivos |-- _configurarSuscripcionDispositivo() --> subscribes to EventChannel |-- obtenerDispositivoActual() --> seeds _dispositivoActualId [NEW] |-- _onDispositivoCambiado() --> first-seen bootstrap + re-resolve |-- audio.aplicarPreset() --> correct preset applied v Settings UI (tap device row) | v showModalBottomSheet |-- TextField (custom name) --> renombrarDispositivo(id, name) | |-> servicio.guardarNombresDispositivos(map) | |-> notifyListeners() | |-- EcualizadorWidget(preset, onCambio) |-> eq.cambiarPresetDispositivo(id, preset) [NEW convenience method] ``` ## File Changes | File | Action | Description | |------|--------|-------------| | `lib/estado/estado_ecualizador.dart` | Modify | Add `obtenerDispositivoActual()` call in `cargarPersistido()` after subscription setup. Add `_nombresDispositivos` map, `renombrarDispositivo()`, `obtenerNombreDispositivo()`, `nombreVisible()` fallback helper. | | `lib/servicios/servicio_ecualizador.dart` | Modify | Add `_keyNombresDispositivos = 'eq_nombres_dispositivos_v1'`. Add `cargarNombresDispositivos()`, `guardarNombresDispositivos()` methods. Load names inside `cargar()` and include in `ConfiguracionEcualizador`. | | `lib/servicios/servicio_ecualizador.dart` (ConfiguracionEcualizador) | Modify | Add `nombresDispositivos` field (`Map`). | | `lib/pantallas/pantalla_ajustes.dart` | Modify | Rewrite `_SeccionEcualizadorAvanzado` device rows: add connection dot (green `CircleAvatar` 8px), display name via fallback chain, `onTap` → `_mostrarEdicionDispositivo()`. Add new `_DialogoEdicionDispositivo` StatefulWidget (bottom sheet body: TextField for name + `EcualizadorWidget`). | | `lib/l10n/app_en.arb` | Modify | Add ~5 new keys: `advancedEqDeviceConnected`, `advancedEqDeviceRenameLabel`, `advancedEqDeviceEditTitle`, `advancedEqDeviceNameHint`, `advancedEqDeviceRenamed`. | | `lib/l10n/app_*.arb` (12 files) | Modify | Translate the new keys to all supported locales. | | `test/` | Create | Tests for: startup device seeding, rename persistence round-trip, display name fallback chain, connection indicator logic. | ## Interfaces / Contracts ```dart // ConfiguracionEcualizador -- add field class ConfiguracionEcualizador { // ... existing fields ... final Map nombresDispositivos; // deviceId -> custom name } // EstadoEcualizador -- new public API Map get nombresDispositivos; Future renombrarDispositivo(String deviceId, String nombre); String obtenerNombreDispositivo(String deviceId); // returns custom name or '' String nombreVisible(String deviceId, String platformName); // fallback chain ``` ```dart // ServicioEcualizador -- new methods Future> cargarNombresDispositivos(); Future guardarNombresDispositivos(Map nombres); ``` ## Testing Strategy | Layer | What to Test | Approach | |-------|-------------|----------| | Unit | `cargarPersistido()` seeds `_dispositivoActualId` when multi-device ON | Fake `ServicioDispositivoAudio` returns known device; assert `dispositivoActualId != null` after load | | Unit | `renombrarDispositivo` persists and notifies | Call rename, verify SP key written and listeners notified | | Unit | `nombreVisible` fallback chain: custom > platform > id | Three cases: all present, no custom, no platform | | Unit | Connection indicator: `entry.key == dispositivoActualId` | Verify boolean derivation in widget test or pure logic | | Widget | Bottom sheet renders rename + EQ | `pumpWidget` with `_DialogoEdicionDispositivo`, verify TextField and `EcualizadorWidget` present | ## Migration / Rollout No migration required. The new SP key `eq_nombres_dispositivos_v1` is created on first rename. If the code is reverted, the key is simply ignored -- no data loss or corruption. The startup fix is additive (one extra method call). ## Open Questions - None blocking. The `nombresDispositivos` omission from export schema v4 is acknowledged and deferred per proposal.