fix(devices): use real Bluetooth MAC as device identity on Android 12+
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:
@@ -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
|
||||||
@@ -693,11 +721,15 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
* Returns a map describing the current active audio output device.
|
* Returns a map describing the current active audio output device.
|
||||||
*
|
*
|
||||||
* Device ID format (matches spec):
|
* Device ID format (matches spec):
|
||||||
* - "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
|
||||||
* - "usb_headset:<address>" — TYPE_USB_HEADSET (14)
|
* - "bt_a2dp:name:<productName>" — TYPE_BLUETOOTH_A2DP (8) fallback when the MAC is absent
|
||||||
* - "builtin_speaker" — fallback when API < 23
|
* 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)
|
||||||
|
* - "builtin_speaker" — fallback when API < 23
|
||||||
*
|
*
|
||||||
* Type int values sent to Dart match the AudioDeviceInfo.TYPE_* constants.
|
* Type int values sent to Dart match the AudioDeviceInfo.TYPE_* constants.
|
||||||
*/
|
*/
|
||||||
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user