fix(devices): use real Bluetooth MAC as device identity on Android 12+
Build & Deploy PluriWave / Análisis de código (push) Successful in 46s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m53s

Without BLUETOOTH_CONNECT, Android 12+ returns the fixed placeholder
02:00:00:00:00:00 for every Bluetooth device's address, so all BT
devices collapsed onto the same equalizer identity and renames
appeared to duplicate devices after re-pairing.

Declare the permission, add a requestBluetoothConnect channel method
mirroring the existing notification-permission flow, guard the
placeholder in deviceToMap() with a colon-sanitized name-based
fallback id (replacing the dead 00:00:00:00:00:00 branch), and
re-emit the active device after the grant so already-connected
devices pick up their real MAC without a reconnect.

Work unit 1/2 of bt-device-identity (Kotlin plumbing).
This commit is contained in:
2026-07-11 00:02:38 +02:00
parent d3763eaec5
commit aef4e02c1f
2 changed files with 50 additions and 7 deletions
+1
View File
@@ -14,6 +14,7 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<application <application
android:label="PluriWave" android:label="PluriWave"
@@ -37,6 +37,8 @@ class MainActivity : AudioServiceActivity() {
private val audioDevicesChannel = "pluriwave/audio_devices" private val audioDevicesChannel = "pluriwave/audio_devices"
private val visualizerPermissionRequestCode = 4821 private val visualizerPermissionRequestCode = 4821
private val notificationPermissionRequestCode = 4822 private val notificationPermissionRequestCode = 4822
private val bluetoothConnectPermissionRequestCode = 4823
private val bluetoothMacPlaceholder = "02:00:00:00:00:00"
private var visualizer: Visualizer? = null private var visualizer: Visualizer? = null
private var pendingSink: EventChannel.EventSink? = null private var pendingSink: EventChannel.EventSink? = null
private var pendingArgs: Map<*, *>? = null private var pendingArgs: Map<*, *>? = null
@@ -614,6 +616,14 @@ class MainActivity : AudioServiceActivity() {
) { ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == notificationPermissionRequestCode) return if (requestCode == notificationPermissionRequestCode) return
if (requestCode == bluetoothConnectPermissionRequestCode) {
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
val device = getActiveAudioDevice()
Log.d(tag, "audio_devices.requestBluetoothConnect granted -> $device")
audioDevicesSink?.success(device)
}
return
}
if (requestCode != visualizerPermissionRequestCode) return if (requestCode != visualizerPermissionRequestCode) return
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) { if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
@@ -641,6 +651,10 @@ class MainActivity : AudioServiceActivity() {
Log.d(tag, "audio_devices.getActiveDevice -> $device") Log.d(tag, "audio_devices.getActiveDevice -> $device")
result.success(device) result.success(device)
} }
"requestBluetoothConnect" -> {
Log.d(tag, "audio_devices.requestBluetoothConnect")
result.success(requestBluetoothConnect())
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@@ -660,6 +674,20 @@ class MainActivity : AudioServiceActivity() {
) )
} }
private fun requestBluetoothConnect(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED
) {
return true
}
requestPermissions(
arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
bluetoothConnectPermissionRequestCode
)
return true
}
private fun registerAudioDeviceCallback() { private fun registerAudioDeviceCallback() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -696,6 +724,10 @@ class MainActivity : AudioServiceActivity() {
* - "builtin_speaker" — TYPE_BUILTIN_SPEAKER (2) * - "builtin_speaker" — TYPE_BUILTIN_SPEAKER (2)
* - "wired_headset" — TYPE_WIRED_HEADSET (3) or TYPE_WIRED_HEADPHONES (4) * - "wired_headset" — TYPE_WIRED_HEADSET (3) or TYPE_WIRED_HEADPHONES (4)
* - "bt_a2dp:<MAC>" — TYPE_BLUETOOTH_A2DP (8); MAC from AudioDeviceInfo.address * - "bt_a2dp:<MAC>" — TYPE_BLUETOOTH_A2DP (8); MAC from AudioDeviceInfo.address
* - "bt_a2dp:name:<productName>" — TYPE_BLUETOOTH_A2DP (8) fallback when the MAC is absent
* or still the OS placeholder ("02:00:00:00:00:00", seen
* without BLUETOOTH_CONNECT); productName colons are
* sanitized to '-' to preserve the matrix-key delimiter
* - "usb_headset:<address>" — TYPE_USB_HEADSET (14) * - "usb_headset:<address>" — TYPE_USB_HEADSET (14)
* - "builtin_speaker" — fallback when API < 23 * - "builtin_speaker" — fallback when API < 23
* *
@@ -739,9 +771,19 @@ class MainActivity : AudioServiceActivity() {
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> AudioDeviceInfo.TYPE_WIRED_HEADPHONES ->
mapOf("id" to "wired_headset", "type" to 3, "name" to (device.productName?.toString() ?: "Wired Headphones")) mapOf("id" to "wired_headset", "type" to 3, "name" to (device.productName?.toString() ?: "Wired Headphones"))
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> { AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> {
val mac = device.address?.takeIf { it.isNotBlank() } ?: "00:00:00:00:00:00" // The OS reports a placeholder MAC ("02:00:00:00:00:00") when
// BLUETOOTH_CONNECT has not been granted; treat that (and any
// null/blank address) as absent instead of using it as an id.
val mac = device.address?.takeIf { it.isNotBlank() && it != bluetoothMacPlaceholder }
val id = if (mac != null) {
"bt_a2dp:$mac"
} else {
val safeProductName = (device.productName?.toString()?.takeIf { it.isNotBlank() } ?: "unknown")
.replace(":", "-")
"bt_a2dp:name:$safeProductName"
}
mapOf( mapOf(
"id" to "bt_a2dp:$mac", "id" to id,
"type" to 8, "type" to 8,
"name" to (device.productName?.toString() ?: "Bluetooth"), "name" to (device.productName?.toString() ?: "Bluetooth"),
) )