Skip to content

Commit 5e78e3a

Browse files
committed
feat: update to new android bluetooth device communication api
1 parent 6d927ef commit 5e78e3a

File tree

1 file changed

+171
-68
lines changed

1 file changed

+171
-68
lines changed

android/src/main/java/com/zxcpoiu/incallmanager/AppRTC/AppRTCBluetoothManager.java

Lines changed: 171 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
import android.content.Intent;
1919
import android.content.IntentFilter;
2020
import android.content.pm.PackageManager;
21+
import android.media.AudioDeviceCallback;
22+
import android.media.AudioDeviceInfo;
2123
import android.media.AudioManager;
2224
import android.os.Build;
2325
import android.os.Handler;
2426
import android.os.Looper;
2527
import android.os.Process;
2628
import android.util.Log;
2729
import androidx.annotation.Nullable;
30+
import androidx.annotation.RequiresApi;
31+
2832
import java.util.List;
2933
import java.util.Set;
3034
import com.zxcpoiu.incallmanager.AppRTC.AppRTCUtils;
@@ -73,6 +77,11 @@ public enum State {
7377
private BluetoothHeadset bluetoothHeadset;
7478
@Nullable
7579
private BluetoothDevice bluetoothDevice;
80+
81+
@Nullable
82+
private AudioDeviceInfo bluetoothAudioDevice;
83+
84+
private AudioDeviceCallback bluetoothAudioDeviceCallback;
7685
private final BroadcastReceiver bluetoothHeadsetReceiver;
7786
// Runs when the Bluetooth timeout expires. We use that timeout after calling
7887
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a
@@ -117,6 +126,34 @@ public void onServiceDisconnected(int profile) {
117126
Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
118127
}
119128
}
129+
130+
@RequiresApi(api = Build.VERSION_CODES.S)
131+
private class BluetoothAudioDeviceCallback extends AudioDeviceCallback {
132+
@Override
133+
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
134+
updateDeviceList();
135+
}
136+
137+
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
138+
updateDeviceList();
139+
}
140+
141+
private void updateDeviceList() {
142+
final AudioDeviceInfo newBtDevice = getScoDevice();
143+
boolean needChange = false;
144+
if (bluetoothAudioDevice != null && newBtDevice == null) {
145+
needChange = true;
146+
} else if (bluetoothAudioDevice == null && newBtDevice != null) {
147+
needChange = true;
148+
} else if (bluetoothAudioDevice != null && bluetoothAudioDevice.getId() != newBtDevice.getId()) {
149+
needChange = true;
150+
}
151+
if (needChange) {
152+
updateAudioDeviceState();
153+
}
154+
}
155+
}
156+
120157
// Intent broadcast receiver which handles changes in Bluetooth device availability.
121158
// Detects headset changes and Bluetooth SCO state changes.
122159
private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {
@@ -198,6 +235,9 @@ protected AppRTCBluetoothManager(Context context, InCallManagerModule audioManag
198235
bluetoothState = State.UNINITIALIZED;
199236
bluetoothServiceListener = new BluetoothServiceListener();
200237
bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
238+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
239+
bluetoothAudioDeviceCallback = new BluetoothAudioDeviceCallback();
240+
}
201241
handler = new Handler(Looper.getMainLooper());
202242
}
203243
/** Returns the internal state. */
@@ -218,6 +258,7 @@ public State getState() {
218258
* Note that the AppRTCAudioManager is also involved in driving this state
219259
* change.
220260
*/
261+
@SuppressLint("MissingPermission")
221262
public void start() {
222263
ThreadUtils.checkIsOnMainThread();
223264
Log.d(TAG, "start");
@@ -252,15 +293,19 @@ public void start() {
252293
Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
253294
return;
254295
}
255-
// Register receivers for BluetoothHeadset change notifications.
256-
IntentFilter bluetoothHeadsetFilter = new IntentFilter();
257-
// Register receiver for change in connection state of the Headset profile.
258-
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
259-
// Register receiver for change in audio connection state of the Headset profile.
260-
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
261-
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
262-
Log.d(TAG, "HEADSET profile state: "
263-
+ stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
296+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
297+
audioManager.registerAudioDeviceCallback(bluetoothAudioDeviceCallback, null);
298+
} else {
299+
// Register receivers for BluetoothHeadset change notifications.
300+
IntentFilter bluetoothHeadsetFilter = new IntentFilter();
301+
// Register receiver for change in connection state of the Headset profile.
302+
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
303+
// Register receiver for change in audio connection state of the Headset profile.
304+
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
305+
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
306+
Log.d(TAG, "HEADSET profile state: "
307+
+ stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
308+
}
264309
Log.d(TAG, "Bluetooth proxy for headset profile has started");
265310
bluetoothState = State.HEADSET_UNAVAILABLE;
266311
Log.d(TAG, "start done: BT state=" + bluetoothState);
@@ -278,8 +323,12 @@ public void stop() {
278323
if (bluetoothState == State.UNINITIALIZED) {
279324
return;
280325
}
281-
unregisterReceiver(bluetoothHeadsetReceiver);
282-
cancelTimer();
326+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
327+
audioManager.unregisterAudioDeviceCallback(bluetoothAudioDeviceCallback);
328+
} else {
329+
unregisterReceiver(bluetoothHeadsetReceiver);
330+
cancelTimer();
331+
}
283332
if (bluetoothHeadset != null) {
284333
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
285334
bluetoothHeadset = null;
@@ -315,18 +364,31 @@ public boolean startScoAudio() {
315364
Log.e(TAG, "BT SCO connection fails - no headset available");
316365
return false;
317366
}
318-
// Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
319-
Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
320-
// The SCO connection establishment can take several seconds, hence we cannot rely on the
321-
// connection to be available when the method returns but instead register to receive the
322-
// intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
323-
bluetoothState = State.SCO_CONNECTING;
324-
audioManager.startBluetoothSco();
325-
audioManager.setBluetoothScoOn(true);
326-
scoConnectionAttempts++;
327-
startTimer();
328-
Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", "
329-
+ "SCO is on: " + isScoOn());
367+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
368+
if (bluetoothAudioDevice != null) {
369+
audioManager.setCommunicationDevice(bluetoothAudioDevice);
370+
bluetoothState = State.SCO_CONNECTED;
371+
Log.d(TAG, "Set bluetooth audio device as communication device: "
372+
+ "id=" + bluetoothAudioDevice.getId());
373+
} else {
374+
bluetoothState = State.SCO_DISCONNECTING;
375+
Log.d(TAG, "Cannot find any bluetooth SCO device to set as communication device");
376+
}
377+
updateAudioDeviceState();
378+
} else {
379+
// The SCO connection establishment can take several seconds, hence we cannot rely on the
380+
// connection to be available when the method returns but instead register to receive the
381+
// intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
382+
// Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
383+
Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
384+
bluetoothState = State.SCO_CONNECTING;
385+
startTimer();
386+
audioManager.startBluetoothSco();
387+
audioManager.setBluetoothScoOn(true);
388+
scoConnectionAttempts++;
389+
Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", "
390+
+ "SCO is on: " + isScoOn());
391+
}
330392
return true;
331393
}
332394
/** Stops Bluetooth SCO connection with remote device. */
@@ -337,9 +399,13 @@ public void stopScoAudio() {
337399
if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
338400
return;
339401
}
340-
cancelTimer();
341-
audioManager.stopBluetoothSco();
342-
audioManager.setBluetoothScoOn(false);
402+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
403+
audioManager.clearCommunicationDevice();
404+
} else {
405+
cancelTimer();
406+
audioManager.stopBluetoothSco();
407+
audioManager.setBluetoothScoOn(false);
408+
}
343409
bluetoothState = State.SCO_DISCONNECTING;
344410
Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", "
345411
+ "SCO is on: " + isScoOn());
@@ -351,27 +417,39 @@ public void stopScoAudio() {
351417
* HEADSET_AVAILABLE and `bluetoothDevice` will be mapped to the connected
352418
* device if available.
353419
*/
420+
@SuppressLint("MissingPermission")
354421
public void updateDevice() {
355422
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
356423
return;
357424
}
358425
Log.d(TAG, "updateDevice");
359-
// Get connected devices for the headset profile. Returns the set of
360-
// devices which are in state STATE_CONNECTED. The BluetoothDevice class
361-
// is just a thin wrapper for a Bluetooth hardware address.
362-
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
363-
if (devices.isEmpty()) {
364-
bluetoothDevice = null;
365-
bluetoothState = State.HEADSET_UNAVAILABLE;
366-
Log.d(TAG, "No connected bluetooth headset");
426+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
427+
bluetoothAudioDevice = getScoDevice();
428+
if (bluetoothAudioDevice != null) {
429+
bluetoothState = State.HEADSET_AVAILABLE;
430+
Log.d(TAG, "Connected bluetooth headset: "
431+
+ "name=" + bluetoothAudioDevice.getProductName());
432+
} else {
433+
bluetoothState = State.HEADSET_UNAVAILABLE;
434+
}
367435
} else {
368-
// Always use first device in list. Android only supports one device.
369-
bluetoothDevice = devices.get(0);
370-
bluetoothState = State.HEADSET_AVAILABLE;
371-
Log.d(TAG, "Connected bluetooth headset: "
372-
+ "name=" + bluetoothDevice.getName() + ", "
373-
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
374-
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
436+
// Get connected devices for the headset profile. Returns the set of
437+
// devices which are in state STATE_CONNECTED. The BluetoothDevice class
438+
// is just a thin wrapper for a Bluetooth hardware address.
439+
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
440+
if (devices.isEmpty()) {
441+
bluetoothDevice = null;
442+
bluetoothState = State.HEADSET_UNAVAILABLE;
443+
Log.d(TAG, "No connected bluetooth headset");
444+
} else {
445+
// Always use first device in list. Android only supports one device.
446+
bluetoothDevice = devices.get(0);
447+
bluetoothState = State.HEADSET_AVAILABLE;
448+
Log.d(TAG, "Connected bluetooth headset: "
449+
+ "name=" + bluetoothDevice.getName() + ", "
450+
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
451+
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
452+
}
375453
}
376454
Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
377455
}
@@ -397,15 +475,15 @@ protected boolean hasPermission(Context context, String permission) {
397475
== PackageManager.PERMISSION_GRANTED;
398476
}
399477
/** Logs the state of the local Bluetooth adapter. */
400-
@SuppressLint("HardwareIds")
478+
@SuppressLint({"HardwareIds", "MissingPermission"})
401479
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
402480
Log.d(TAG, "BluetoothAdapter: "
403481
+ "enabled=" + localAdapter.isEnabled() + ", "
404482
+ "state=" + stateToString(localAdapter.getState()) + ", "
405483
+ "name=" + localAdapter.getName() + ", "
406484
+ "address=" + localAdapter.getAddress());
407485
// Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
408-
Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
486+
Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
409487
if (!pairedDevices.isEmpty()) {
410488
Log.d(TAG, "paired devices:");
411489
for (BluetoothDevice device : pairedDevices) {
@@ -435,44 +513,54 @@ private void cancelTimer() {
435513
* Called when start of the BT SCO channel takes too long time. Usually
436514
* happens when the BT device has been turned on during an ongoing call.
437515
*/
516+
@SuppressLint("MissingPermission")
438517
private void bluetoothTimeout() {
439518
ThreadUtils.checkIsOnMainThread();
440519
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
441520
return;
442521
}
443-
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
444-
+ "attempts: " + scoConnectionAttempts + ", "
445-
+ "SCO is on: " + isScoOn());
446-
if (bluetoothState != State.SCO_CONNECTING) {
447-
return;
448-
}
449-
// Bluetooth SCO should be connecting; check the latest result.
450-
boolean scoConnected = false;
451-
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
452-
if (devices.size() > 0) {
453-
bluetoothDevice = devices.get(0);
454-
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
455-
Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
456-
scoConnected = true;
522+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
523+
Log.w(TAG, "Invalid state, the timeout should not be running on the version: " + Build.VERSION.SDK_INT);
524+
} else {
525+
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
526+
+ "attempts: " + scoConnectionAttempts + ", "
527+
+ "SCO is on: " + isScoOn());
528+
if (bluetoothState != State.SCO_CONNECTING) {
529+
return;
530+
}
531+
// Bluetooth SCO should be connecting; check the latest result.
532+
boolean scoConnected = false;
533+
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
534+
if (devices.size() > 0) {
535+
bluetoothDevice = devices.get(0);
536+
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
537+
Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
538+
scoConnected = true;
539+
} else {
540+
Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
541+
}
542+
}
543+
if (scoConnected) {
544+
// We thought BT had timed out, but it's actually on; updating state.
545+
bluetoothState = State.SCO_CONNECTED;
546+
scoConnectionAttempts = 0;
457547
} else {
458-
Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
548+
// Give up and "cancel" our request by calling stopBluetoothSco().
549+
Log.w(TAG, "BT failed to connect after timeout");
550+
stopScoAudio();
459551
}
460552
}
461-
if (scoConnected) {
462-
// We thought BT had timed out, but it's actually on; updating state.
463-
bluetoothState = State.SCO_CONNECTED;
464-
scoConnectionAttempts = 0;
465-
} else {
466-
// Give up and "cancel" our request by calling stopBluetoothSco().
467-
Log.w(TAG, "BT failed to connect after timeout");
468-
stopScoAudio();
469-
}
470553
updateAudioDeviceState();
471554
Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
472555
}
473556
/** Checks whether audio uses Bluetooth SCO. */
474557
private boolean isScoOn() {
475-
return audioManager.isBluetoothScoOn();
558+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
559+
AudioDeviceInfo communicationDevice = audioManager.getCommunicationDevice();
560+
return communicationDevice != null && bluetoothAudioDevice != null && communicationDevice.getId() == bluetoothAudioDevice.getId();
561+
} else {
562+
return audioManager.isBluetoothScoOn();
563+
}
476564
}
477565
/** Converts BluetoothAdapter states into local string representations. */
478566
private String stateToString(int state) {
@@ -501,4 +589,19 @@ private String stateToString(int state) {
501589
return "INVALID";
502590
}
503591
}
592+
593+
@Nullable
594+
@RequiresApi(api = Build.VERSION_CODES.S)
595+
private AudioDeviceInfo getScoDevice() {
596+
if (audioManager != null) {
597+
List<AudioDeviceInfo> devices = audioManager.getAvailableCommunicationDevices();
598+
for (AudioDeviceInfo device : devices) {
599+
if (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET
600+
|| device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
601+
return device;
602+
}
603+
}
604+
}
605+
return null;
606+
}
504607
}

0 commit comments

Comments
 (0)