Skip to content

Commit fe41a34

Browse files
authored
fix: do not set invalid BT devices as communication device (#2064)
### 💡 Overview Currently we set A2DP BT devices as communication device even if they dont have headset profile, this PR adds some safeguards and filtering to prevent this
1 parent 04ca858 commit fe41a34

File tree

3 files changed

+53
-8
lines changed

3 files changed

+53
-8
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"build:video-filters-react-native": "yarn workspace @stream-io/video-filters-react-native run build",
3131
"build:noise-cancellation-react-native": "yarn workspace @stream-io/noise-cancellation-react-native run build",
3232
"build:react:deps": "yarn workspaces foreach -Apv --topological-dev --include 'packages/{client,react-{sdk,bindings},styling,{video,audio}-filters-web}' run build",
33-
"build:react-native:deps": "yarn workspaces foreach -Apv --topological-dev --include 'packages/{client,react-bindings,{video-filters,noise-cancellation}-react-native,react-native-sdk}' run build",
33+
"build:react-native:deps": "yarn workspaces foreach -Apv --topological-dev --include 'packages/{client,react-bindings,audio-filters-web,{video-filters,noise-cancellation}-react-native,react-native-sdk}' run build",
3434
"build:vercel": "yarn build:react:deps && yarn build:react:dogfood",
3535
"start:egress": "yarn workspace @stream-io/egress-composite start",
3636
"build:egress": "yarn workspace @stream-io/egress-composite build",

packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/audio/utils/AudioDeviceEndpointUtils.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,22 @@ internal class AudioDeviceEndpointUtils {
4444
val endpoints: MutableList<AudioDeviceEndpoint> = mutableListOf()
4545
var foundWiredHeadset = false
4646
val omittedDevices = StringBuilder("omitting devices =[")
47-
adiArr.toList().forEach { audioDeviceInfo ->
47+
adiArr.forEach { audioDeviceInfo ->
48+
/**
49+
* Only devices in a sink role can be selected via AudioManager#setCommunicationDevice
50+
* (API 31+). We enforce this at the endpoint construction level as a safety net
51+
* because AudioDeviceCallback can surface devices that aren't valid call endpoints.
52+
*/
53+
val isInvalidCallEndpoint = !audioDeviceInfo.isSink
54+
55+
if (isInvalidCallEndpoint) {
56+
omittedDevices.append(
57+
"(type=[${audioDeviceInfo.type}]," +
58+
" name=[${audioDeviceInfo.productName}]),"
59+
)
60+
return@forEach
61+
}
62+
4863
val endpoint = getEndpointFromAudioDeviceInfo(audioDeviceInfo)
4964
if (endpoint.type != AudioDeviceEndpoint.TYPE_UNKNOWN) {
5065
if (endpoint.type == AudioDeviceEndpoint.TYPE_WIRED_HEADSET) {
@@ -115,11 +130,8 @@ internal class AudioDeviceEndpointUtils {
115130
AudioDeviceInfo.TYPE_USB_HEADSET -> AudioDeviceEndpoint.TYPE_WIRED_HEADSET
116131
// Bluetooth Devices
117132
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
118-
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
119133
AudioDeviceInfo.TYPE_HEARING_AID,
120-
AudioDeviceInfo.TYPE_BLE_HEADSET,
121-
AudioDeviceInfo.TYPE_BLE_SPEAKER,
122-
AudioDeviceInfo.TYPE_BLE_BROADCAST -> AudioDeviceEndpoint.TYPE_BLUETOOTH
134+
AudioDeviceInfo.TYPE_BLE_HEADSET -> AudioDeviceEndpoint.TYPE_BLUETOOTH
123135
// Everything else is defaulted to TYPE_UNKNOWN
124136
else -> AudioDeviceEndpoint.TYPE_UNKNOWN
125137
}

packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/audio/utils/AudioManagerUtil.kt

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,39 @@ internal class AudioManagerUtil {
2525
}
2626
}
2727

28+
/**
29+
* Safe wrapper around [AudioManager.setCommunicationDevice] to avoid crashing the app on
30+
* OEM-specific edge cases. On API 31+, Android requires that:
31+
* - the device is a sink (output), and
32+
* - the device is among [AudioManager.availableCommunicationDevices].
33+
*/
34+
@RequiresApi(31)
35+
fun setCommunicationDeviceSafely(
36+
audioManager: AudioManager,
37+
deviceInfo: AudioDeviceInfo,
38+
): Boolean {
39+
40+
if (!deviceInfo.isSink) {
41+
Log.w(TAG, "setCommunicationDeviceSafely: rejecting non-sink device type=${deviceInfo.type}, id=${deviceInfo.id}")
42+
return false
43+
}
44+
45+
val available = audioManager.availableCommunicationDevices
46+
val isAvailable = available.any { it.id == deviceInfo.id }
47+
if (!isAvailable) {
48+
Log.w(TAG, "setCommunicationDeviceSafely: device not in availableCommunicationDevices type=${deviceInfo.type}, id=${deviceInfo.id}")
49+
return false
50+
}
51+
52+
return try {
53+
audioManager.setCommunicationDevice(deviceInfo)
54+
true
55+
} catch (e: IllegalArgumentException) {
56+
Log.w(TAG, "setCommunicationDeviceSafely: failed type=${deviceInfo.type}, id=${deviceInfo.id}", e)
57+
false
58+
}
59+
}
60+
2861
fun isSpeakerphoneOn(audioManager: AudioManager): Boolean {
2962
return if (Build.VERSION.SDK_INT >= 31) {
3063
AudioManager31PlusImpl.isSpeakerphoneOn(audioManager)
@@ -83,7 +116,7 @@ internal class AudioManagerUtil {
83116
endpointMaps.nonBluetoothEndpoints.values.firstOrNull {
84117
it.type == deviceType
85118
}?.let {
86-
audioManager.setCommunicationDevice(it.deviceInfo)
119+
setCommunicationDeviceSafely(audioManager, it.deviceInfo)
87120
bluetoothManager.updateDevice()
88121
return endpointMaps.nonBluetoothEndpoints[deviceType]
89122
}
@@ -92,7 +125,7 @@ internal class AudioManagerUtil {
92125
AudioDeviceEndpoint.TYPE_SPEAKER -> {
93126
val speakerDevice = endpointMaps.nonBluetoothEndpoints[AudioDeviceEndpoint.TYPE_SPEAKER]
94127
speakerDevice?.let {
95-
audioManager.setCommunicationDevice(it.deviceInfo)
128+
setCommunicationDeviceSafely(audioManager, it.deviceInfo)
96129
bluetoothManager.updateDevice()
97130
return speakerDevice
98131
}

0 commit comments

Comments
 (0)