18
18
import android .content .Intent ;
19
19
import android .content .IntentFilter ;
20
20
import android .content .pm .PackageManager ;
21
+ import android .media .AudioDeviceCallback ;
22
+ import android .media .AudioDeviceInfo ;
21
23
import android .media .AudioManager ;
22
24
import android .os .Build ;
23
25
import android .os .Handler ;
24
26
import android .os .Looper ;
25
27
import android .os .Process ;
26
28
import android .util .Log ;
27
29
import androidx .annotation .Nullable ;
30
+ import androidx .annotation .RequiresApi ;
31
+
28
32
import java .util .List ;
29
33
import java .util .Set ;
30
34
import com .zxcpoiu .incallmanager .AppRTC .AppRTCUtils ;
@@ -73,6 +77,11 @@ public enum State {
73
77
private BluetoothHeadset bluetoothHeadset ;
74
78
@ Nullable
75
79
private BluetoothDevice bluetoothDevice ;
80
+
81
+ @ Nullable
82
+ private AudioDeviceInfo bluetoothAudioDevice ;
83
+
84
+ private AudioDeviceCallback bluetoothAudioDeviceCallback ;
76
85
private final BroadcastReceiver bluetoothHeadsetReceiver ;
77
86
// Runs when the Bluetooth timeout expires. We use that timeout after calling
78
87
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a
@@ -117,6 +126,34 @@ public void onServiceDisconnected(int profile) {
117
126
Log .d (TAG , "onServiceDisconnected done: BT state=" + bluetoothState );
118
127
}
119
128
}
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
+
120
157
// Intent broadcast receiver which handles changes in Bluetooth device availability.
121
158
// Detects headset changes and Bluetooth SCO state changes.
122
159
private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {
@@ -198,6 +235,9 @@ protected AppRTCBluetoothManager(Context context, InCallManagerModule audioManag
198
235
bluetoothState = State .UNINITIALIZED ;
199
236
bluetoothServiceListener = new BluetoothServiceListener ();
200
237
bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver ();
238
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
239
+ bluetoothAudioDeviceCallback = new BluetoothAudioDeviceCallback ();
240
+ }
201
241
handler = new Handler (Looper .getMainLooper ());
202
242
}
203
243
/** Returns the internal state. */
@@ -218,6 +258,7 @@ public State getState() {
218
258
* Note that the AppRTCAudioManager is also involved in driving this state
219
259
* change.
220
260
*/
261
+ @ SuppressLint ("MissingPermission" )
221
262
public void start () {
222
263
ThreadUtils .checkIsOnMainThread ();
223
264
Log .d (TAG , "start" );
@@ -252,15 +293,19 @@ public void start() {
252
293
Log .e (TAG , "BluetoothAdapter.getProfileProxy(HEADSET) failed" );
253
294
return ;
254
295
}
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
+ }
264
309
Log .d (TAG , "Bluetooth proxy for headset profile has started" );
265
310
bluetoothState = State .HEADSET_UNAVAILABLE ;
266
311
Log .d (TAG , "start done: BT state=" + bluetoothState );
@@ -278,8 +323,12 @@ public void stop() {
278
323
if (bluetoothState == State .UNINITIALIZED ) {
279
324
return ;
280
325
}
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
+ }
283
332
if (bluetoothHeadset != null ) {
284
333
bluetoothAdapter .closeProfileProxy (BluetoothProfile .HEADSET , bluetoothHeadset );
285
334
bluetoothHeadset = null ;
@@ -315,18 +364,31 @@ public boolean startScoAudio() {
315
364
Log .e (TAG , "BT SCO connection fails - no headset available" );
316
365
return false ;
317
366
}
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
+ }
330
392
return true ;
331
393
}
332
394
/** Stops Bluetooth SCO connection with remote device. */
@@ -337,9 +399,13 @@ public void stopScoAudio() {
337
399
if (bluetoothState != State .SCO_CONNECTING && bluetoothState != State .SCO_CONNECTED ) {
338
400
return ;
339
401
}
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
+ }
343
409
bluetoothState = State .SCO_DISCONNECTING ;
344
410
Log .d (TAG , "stopScoAudio done: BT state=" + bluetoothState + ", "
345
411
+ "SCO is on: " + isScoOn ());
@@ -351,27 +417,39 @@ public void stopScoAudio() {
351
417
* HEADSET_AVAILABLE and `bluetoothDevice` will be mapped to the connected
352
418
* device if available.
353
419
*/
420
+ @ SuppressLint ("MissingPermission" )
354
421
public void updateDevice () {
355
422
if (bluetoothState == State .UNINITIALIZED || bluetoothHeadset == null ) {
356
423
return ;
357
424
}
358
425
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
+ }
367
435
} 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
+ }
375
453
}
376
454
Log .d (TAG , "updateDevice done: BT state=" + bluetoothState );
377
455
}
@@ -397,15 +475,15 @@ protected boolean hasPermission(Context context, String permission) {
397
475
== PackageManager .PERMISSION_GRANTED ;
398
476
}
399
477
/** Logs the state of the local Bluetooth adapter. */
400
- @ SuppressLint ("HardwareIds" )
478
+ @ SuppressLint ({ "HardwareIds" , "MissingPermission" } )
401
479
protected void logBluetoothAdapterInfo (BluetoothAdapter localAdapter ) {
402
480
Log .d (TAG , "BluetoothAdapter: "
403
481
+ "enabled=" + localAdapter .isEnabled () + ", "
404
482
+ "state=" + stateToString (localAdapter .getState ()) + ", "
405
483
+ "name=" + localAdapter .getName () + ", "
406
484
+ "address=" + localAdapter .getAddress ());
407
485
// 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 ();
409
487
if (!pairedDevices .isEmpty ()) {
410
488
Log .d (TAG , "paired devices:" );
411
489
for (BluetoothDevice device : pairedDevices ) {
@@ -435,44 +513,54 @@ private void cancelTimer() {
435
513
* Called when start of the BT SCO channel takes too long time. Usually
436
514
* happens when the BT device has been turned on during an ongoing call.
437
515
*/
516
+ @ SuppressLint ("MissingPermission" )
438
517
private void bluetoothTimeout () {
439
518
ThreadUtils .checkIsOnMainThread ();
440
519
if (bluetoothState == State .UNINITIALIZED || bluetoothHeadset == null ) {
441
520
return ;
442
521
}
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 ;
457
547
} 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 ();
459
551
}
460
552
}
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
- }
470
553
updateAudioDeviceState ();
471
554
Log .d (TAG , "bluetoothTimeout done: BT state=" + bluetoothState );
472
555
}
473
556
/** Checks whether audio uses Bluetooth SCO. */
474
557
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
+ }
476
564
}
477
565
/** Converts BluetoothAdapter states into local string representations. */
478
566
private String stateToString (int state ) {
@@ -501,4 +589,19 @@ private String stateToString(int state) {
501
589
return "INVALID" ;
502
590
}
503
591
}
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
+ }
504
607
}
0 commit comments