Move the change folder to changes/archive/2026-07-11-bt-device-identity with the verified artifact set (verdict: pass with warnings, 0 critical, 102/102 targeted tests) and create the bt-device-identity capability spec under openspec/specs/. Phase 7 on-device QA remains the pending human gate before release.
3.5 KiB
Exploration: bt-device-identity
Root Cause — two independent, compounding bugs
Bug 1 — Missing BLUETOOTH_CONNECT permission (Android 12+/API 31+)
AndroidManifest.xml declares zero Bluetooth permissions. On API 31+, without BLUETOOTH_CONNECT, AudioDeviceInfo.getAddress() for BT devices returns the fixed placeholder "02:00:00:00:00:00" — not null, not empty, no exception. In MainActivity.kt deviceToMap() (L742), the guard takeIf { it.isNotBlank() } lets the placeholder pass through since it is non-blank. Result: every BT A2DP device collapses onto the identical id bt_a2dp:02:00:00:00:00:00. No runtime BT permission request flow exists anywhere in the Kotlin code (only RECORD_AUDIO/POST_NOTIFICATIONS have flows) — completely silent failure. Origin: multi-device-eq/design.md L17 claimed "no extra permission needed".
Bug 2 — Duplicate device-list entry on any never-before-seen deviceId
estado_ecualizador.dart _onDispositivoCambiado() (L210-227) creates a new _presetsDispositivo map entry whenever the incoming dispositivo.id isn't already a key. The "known devices" list in pantalla_ajustes.dart is keyed off this map; the rename overlay (_nombresDispositivos) is a SEPARATE map also keyed by deviceId — a rename under one id string does nothing for a different id string. This produces the literal "duplicate after re-pairing" symptom (likely via transient builtin_speaker/wired_headset reports during A2DP reconnect handshake) and gets structurally worse once Bug 1 is fixed until placeholder entries are cleaned.
Device name (symptoms 2/4) — smaller fix than expected
productName already flows end-to-end from Kotlin into DispositivoAudio.nombre. But pantalla_ajustes.dart calls eq.nombreVisible(deviceId, '') at both call sites (L769, L846) — always passing an empty platform name — and EstadoEcualizador never caches per-device platform names. The fallback chain in nombreVisible() is correct; it's just starved of input.
iOS is already the reference pattern
AudioDevicesPlugin.swift stableKey() (L153-165) uses uid as primary key with portType+portName fallback when uid is empty — exactly the pattern Android needs. No iOS changes required.
Recommended fix (5 points)
- Declare
BLUETOOTH_CONNECTin manifest + runtime request flow (mirror existing POST_NOTIFICATIONS pattern) - Detect the
02:00:00:00:00:00placeholder indeviceToMap()and fall back to type+productName composite id instead - Cache per-device platform names in
EstadoEcualizadorsonombreVisible()gets real input - MAC remains the canonical id where available (id format unchanged — preserves colon-delimiter key safety from multi-device-eq design L35)
- Migration: detect and DROP placeholder-keyed entries (collision destroyed per-device info at capture time — unrecoverable), keep legitimate stable-MAC entries as-is, one-time "please rename your devices again" notice for affected users only
Risks
- Migration is destructive by necessity for the placeholder-collision subset
- New runtime-request flow has no BT precedent in codebase (copy RECORD_AUDIO/POST_NOTIFICATIONS patterns)
- Any id-format change must preserve
eq_presets_matriz_v1colon-delimiter key safety
Files (read during exploration)
dispositivo_audio.dart, servicio_dispositivo_audio.dart, MainActivity.kt, AndroidManifest.xml, estado_ecualizador.dart, servicio_ecualizador.dart, pantalla_ajustes.dart, AudioDevicesPlugin.swift