diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 39f7c12..ba0614e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,14 @@
+
+
{
+ val names = bondedDeviceNames()
+ Log.d(tag, "audio_devices.getBondedDeviceNames count=${names.size}")
+ result.success(names)
+ }
else -> result.notImplemented()
}
}
@@ -1122,6 +1128,46 @@ class MainActivity : AudioServiceActivity() {
return true
}
+ /**
+ * Returns MAC -> name for every PAIRED Bluetooth device, connected or not.
+ *
+ * `AudioDeviceInfo.productName` only exists while a device is enumerated as
+ * an active output, so a paired-but-switched-off device can never report its
+ * own name and its row falls back to the raw id. The bond list is the
+ * system's own record and is the only source that survives disconnection.
+ *
+ * Returns an empty map instead of throwing when the answer is unavailable
+ * (BLUETOOTH_CONNECT denied, no adapter, device with Bluetooth off): a
+ * missing name must degrade to the id, never break device resolution.
+ */
+ private fun bondedDeviceNames(): Map {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ Log.d(tag, "audio_devices.bondedDeviceNames BLUETOOTH_CONNECT not granted")
+ return emptyMap()
+ }
+ return try {
+ val manager = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
+ val adapter = manager?.adapter ?: return emptyMap()
+ adapter.bondedDevices
+ .orEmpty()
+ .mapNotNull { device ->
+ val address = device.address
+ ?.takeIf { it.isNotBlank() && it != bluetoothMacPlaceholder }
+ ?: return@mapNotNull null
+ val name = device.name?.takeIf { it.isNotBlank() }
+ ?: return@mapNotNull null
+ address.uppercase() to name
+ }
+ .toMap()
+ } catch (error: Throwable) {
+ Log.w(tag, "audio_devices.bondedDeviceNames failed", error)
+ emptyMap()
+ }
+ }
+
private fun registerAudioDeviceCallback() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
diff --git a/lib/estado/estado_ecualizador.dart b/lib/estado/estado_ecualizador.dart
index f757b46..c262bca 100644
--- a/lib/estado/estado_ecualizador.dart
+++ b/lib/estado/estado_ecualizador.dart
@@ -176,6 +176,7 @@ class EstadoEcualizador extends ChangeNotifier {
// Seed current device immediately so presets resolve before first event.
if (_eqMultiDeviceEnabled) {
+ await _sembrarNombresEmparejados();
await _sembrarDispositivoActual();
}
} catch (_) {
@@ -190,6 +191,30 @@ class EstadoEcualizador extends ChangeNotifier {
}
}
+ /// Fills [_nombresPlataforma] from the system's paired-device list.
+ ///
+ /// A Bluetooth device only reports its own name while it is connected, so
+ /// without this a device the user never renamed shows its raw id whenever it
+ /// is switched off — which is most of the time. The bond list is the system's
+ /// own record and survives disconnection.
+ ///
+ /// Seeded BEFORE [_sembrarDispositivoActual] so a live enumeration name (the
+ /// fresher of the two) overwrites the paired one rather than the reverse.
+ /// Never throws: a device with no resolvable name just falls back to its id.
+ Future _sembrarNombresEmparejados() async {
+ final svc = _dispositivoAudio;
+ if (svc == null) return;
+ try {
+ final emparejados = await svc.obtenerNombresEmparejados();
+ for (final entry in emparejados.entries) {
+ if (entry.value.isEmpty) continue;
+ _nombresPlataforma['bt_a2dp:${entry.key}'] = entry.value;
+ }
+ } catch (_) {
+ // Permission denied or no adapter: keep whatever names we already have.
+ }
+ }
+
/// Queries the current device and seeds [_dispositivoActualId] without
/// waiting for a stream event. Falls back to `'builtin_speaker'` on error.
Future _sembrarDispositivoActual() async {
@@ -232,6 +257,10 @@ class EstadoEcualizador extends ChangeNotifier {
} catch (_) {
// A failed resubscribe must never block the fresh-query re-seed below.
}
+ // Re-read the bond list too: the user may have paired or renamed a device
+ // in system settings since the app started, and this runs right as the
+ // device list becomes visible.
+ await _sembrarNombresEmparejados();
await _sembrarDispositivoActual();
}
diff --git a/lib/servicios/servicio_dispositivo_audio.dart b/lib/servicios/servicio_dispositivo_audio.dart
index 303f849..080d600 100644
--- a/lib/servicios/servicio_dispositivo_audio.dart
+++ b/lib/servicios/servicio_dispositivo_audio.dart
@@ -55,6 +55,19 @@ abstract class ServicioDispositivoAudio {
/// denied.
Future solicitarPermisoBluetooth();
+ /// Returns the names the system remembers for every PAIRED Bluetooth device,
+ /// keyed by uppercase MAC.
+ ///
+ /// `AudioDeviceInfo.productName` only exists for devices the platform is
+ /// enumerating right now, i.e. currently connected ones, so a paired device
+ /// sitting in a drawer can never report its own name. The bond list is the
+ /// authoritative source that survives disconnection.
+ ///
+ /// Returns an empty map when the answer is unavailable rather than throwing:
+ /// `BLUETOOTH_CONNECT` may be denied, and a missing name must degrade to the
+ /// id fallback, never break device resolution.
+ Future