1818import android .content .Intent ;
1919import android .content .IntentFilter ;
2020import android .content .pm .PackageManager ;
21+ import android .media .AudioDeviceCallback ;
22+ import android .media .AudioDeviceInfo ;
2123import android .media .AudioManager ;
2224import android .os .Build ;
2325import android .os .Handler ;
2426import android .os .Looper ;
2527import android .os .Process ;
2628import android .util .Log ;
2729import androidx .annotation .Nullable ;
30+ import androidx .annotation .RequiresApi ;
31+
2832import java .util .List ;
2933import java .util .Set ;
3034import 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