Skip to content

Commit 8e6661c

Browse files
author
John Qualls
authored
Add support for multiple connected bluetooth headsets (#40)
* First attempt at implementing a Bluetooth Headset State * Only set headset state back to connected if is not the active device * Only change bluetooth state if is a new value * Add audio device fallback mechanism in event of sco activation timeout * Convert HeadsetState from singleton to normal class * Fix unit test compilation errors * Add activating state and restart bluetooth sco when the active device is changed * Pull out cache and display generic bluetooth name for now * Add new ActivationError state to handle sco error scenario * Display bluetooth headset name WIP * Fix unit test compilation errors and move common dependency logic to the BaseTest class * Rename HeadsetState.kt -> BluetoothHeadsetState.kt * Move bluetooth headset state management into the BluetoothHeadsetManager * Change naming of states * Bluetooth headset refactor (#42) * Consolidate all headset logic into the BluetoothHeadsetManager * Add comments explaining various blueotooth headset states * Add logs for invalid state tranisition attempts * Fix unit tests after refactor * Fix E2E tests after refactor * Add warning logs for activate and deactivate invalid state transition attempts * Add changelog update for latest changes * Update minor version * Add CL entry to README * Add BT settings clause
1 parent 99f2f80 commit 8e6661c

24 files changed

+837
-1170
lines changed

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Changelog
22

3-
### 0.1.6
3+
### 0.2.0
4+
5+
Enhancements
6+
- Added support for multiple connected bluetooth headsets.
7+
- The library will now accurately display the up to date active bluetooth headset within the `AudiodDeviceSelector` `availableAudioDevices` and `selectedAudioDevice` functions.
8+
- Other connected headsets are not stored by the library at this moment.
9+
- In the event of a failure to connecting audio to a bluetooth headset, the library will revert the selected audio device (this is usually the Earpiece on a phone).
10+
- If a user would like to switch between multiple Bluetooth headsets, then they need to switch the active bluetooth headset from the system Bluetooth settings.
11+
- The newly activated headset will be propagated to the `AudiodDeviceSelector` `availableAudioDevices` and `selectedAudioDevice` functions.
412

513
Bug Fixes
614

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ audioDeviceSelector.deactivate()
8787
```
8888
**Note:** The `stop()` function will call `deactivate()` before closing AudioDeviceSelector resources.
8989

90+
## Bluetooth Support
91+
92+
Multiple connected bluetooth headsets are supported.
93+
- The library will accurately display the up to date active bluetooth headset within the `AudiodDeviceSelector` `availableAudioDevices` and `selectedAudioDevice` functions.
94+
- Other connected headsets are not stored by the library at this moment.
95+
- In the event of a failure to connecting audio to a bluetooth headset, the library will revert the selected audio device (this is usually the Earpiece on a phone).
96+
- If a user would like to switch between multiple Bluetooth headsets, then they need to switch the active bluetooth headset from the system Bluetooth settings.
97+
- The newly activated headset will be propagated to the `AudiodDeviceSelector` `availableAudioDevices` and `selectedAudioDevice` functions.
98+
9099
## Usage Examples
91100

92101
* [Twilio Video Android App](https://github.com/twilio/twilio-video-app-android)
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.twilio.audioswitch
22

3-
import android.bluetooth.BluetoothDevice
43
import androidx.test.annotation.UiThreadTest
54
import androidx.test.ext.junit.runners.AndroidJUnit4
65
import androidx.test.filters.LargeTest
@@ -33,42 +32,4 @@ class MultipleBluetoothHeadsetsTest {
3332
`is`(nullValue()))
3433
assertThat(isSpeakerPhoneOn(), equalTo(false)) // Best we can do for asserting if a fake BT headset is activated
3534
}
36-
37-
@UiThreadTest
38-
@Test
39-
fun `it_should_assert_the_first_bluetooth_headset_when_two_are_connected_and_the_second_is_disconnected`() {
40-
val (audioDeviceSelector, bluetoothHeadsetReceiver) = setupFakeAudioDeviceSelector(getInstrumentationContext())
41-
audioDeviceSelector.start { _, _ -> }
42-
audioDeviceSelector.activate()
43-
simulateBluetoothSystemIntent(getInstrumentationContext(), bluetoothHeadsetReceiver)
44-
simulateBluetoothSystemIntent(getInstrumentationContext(), bluetoothHeadsetReceiver, HEADSET_2_NAME)
45-
46-
simulateBluetoothSystemIntent(getInstrumentationContext(), bluetoothHeadsetReceiver, HEADSET_2_NAME,
47-
BluetoothDevice.ACTION_ACL_DISCONNECTED)
48-
49-
assertThat(audioDeviceSelector.selectedAudioDevice!!.name, equalTo(HEADSET_NAME))
50-
assertThat(audioDeviceSelector.availableAudioDevices.first().name, equalTo(HEADSET_NAME))
51-
assertThat(audioDeviceSelector.availableAudioDevices.find { it.name == HEADSET_2_NAME },
52-
`is`(nullValue()))
53-
assertThat(isSpeakerPhoneOn(), equalTo(false))
54-
}
55-
56-
@UiThreadTest
57-
@Test
58-
fun `it_should_assert_the_second_bluetooth_headset_when_two_are_connected_and_the_first_is_disconnected`() {
59-
val (audioDeviceSelector, bluetoothHeadsetReceiver) = setupFakeAudioDeviceSelector(getInstrumentationContext())
60-
audioDeviceSelector.start { _, _ -> }
61-
audioDeviceSelector.activate()
62-
simulateBluetoothSystemIntent(getInstrumentationContext(), bluetoothHeadsetReceiver)
63-
simulateBluetoothSystemIntent(getInstrumentationContext(), bluetoothHeadsetReceiver, HEADSET_2_NAME)
64-
65-
simulateBluetoothSystemIntent(getInstrumentationContext(), bluetoothHeadsetReceiver, HEADSET_NAME,
66-
BluetoothDevice.ACTION_ACL_DISCONNECTED)
67-
68-
assertThat(audioDeviceSelector.selectedAudioDevice!!.name, equalTo(HEADSET_2_NAME))
69-
assertThat(audioDeviceSelector.availableAudioDevices.first().name, equalTo(HEADSET_2_NAME))
70-
assertThat(audioDeviceSelector.availableAudioDevices.find { it.name == HEADSET_NAME },
71-
`is`(nullValue()))
72-
assertThat(isSpeakerPhoneOn(), equalTo(false))
73-
}
7435
}

audioswitch/src/androidTest/java/com.twilio.audioswitch/TestUtil.kt

+8-19
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,14 @@ import com.twilio.audioswitch.android.DEVICE_NAME
1111
import com.twilio.audioswitch.android.FakeBluetoothIntentProcessor
1212
import com.twilio.audioswitch.android.HEADSET_NAME
1313
import com.twilio.audioswitch.android.LogWrapper
14-
import com.twilio.audioswitch.bluetooth.BluetoothController
15-
import com.twilio.audioswitch.bluetooth.BluetoothHeadsetCacheManager
1614
import com.twilio.audioswitch.bluetooth.BluetoothHeadsetManager
17-
import com.twilio.audioswitch.bluetooth.BluetoothHeadsetReceiver
1815
import com.twilio.audioswitch.selection.AudioDeviceManager
1916
import com.twilio.audioswitch.selection.AudioDeviceSelector
2017
import com.twilio.audioswitch.selection.AudioFocusRequestWrapper
2118
import com.twilio.audioswitch.wired.WiredHeadsetReceiver
2219

23-
val TAG = "TestUtil"
24-
2520
internal fun setupFakeAudioDeviceSelector(context: Context):
26-
Pair<AudioDeviceSelector, BluetoothHeadsetReceiver> {
21+
Pair<AudioDeviceSelector, BluetoothHeadsetManager> {
2722

2823
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
2924
val logger = LogWrapper()
@@ -34,35 +29,29 @@ internal fun setupFakeAudioDeviceSelector(context: Context):
3429
BuildWrapper(),
3530
AudioFocusRequestWrapper())
3631
val wiredHeadsetReceiver = WiredHeadsetReceiver(context, logger)
37-
val bluetoothIntentProcessor = FakeBluetoothIntentProcessor()
38-
val deviceCache = BluetoothHeadsetCacheManager(logger)
39-
val bluetoothHeadsetReceiver = BluetoothHeadsetReceiver(context, logger, bluetoothIntentProcessor, audioDeviceManager, deviceCache)
40-
val bluetoothController = BluetoothAdapter.getDefaultAdapter()?.let { bluetoothAdapter ->
41-
BluetoothController(context,
42-
bluetoothAdapter,
43-
BluetoothHeadsetManager(logger, bluetoothAdapter, deviceCache),
44-
bluetoothHeadsetReceiver)
32+
val headsetManager = BluetoothAdapter.getDefaultAdapter()?.let { bluetoothAdapter ->
33+
BluetoothHeadsetManager(context, logger, bluetoothAdapter, audioDeviceManager,
34+
bluetoothIntentProcessor = FakeBluetoothIntentProcessor())
4535
} ?: run {
4636
null
4737
}
4838
return Pair(AudioDeviceSelector(logger,
4939
audioDeviceManager,
5040
wiredHeadsetReceiver,
51-
bluetoothController,
52-
deviceCache),
53-
bluetoothHeadsetReceiver)
41+
headsetManager),
42+
headsetManager!!)
5443
}
5544

5645
internal fun simulateBluetoothSystemIntent(
5746
context: Context,
58-
bluetoothHeadsetReceiver: BluetoothHeadsetReceiver,
47+
headsetManager: BluetoothHeadsetManager,
5948
deviceName: String = HEADSET_NAME,
6049
action: String = BluetoothDevice.ACTION_ACL_CONNECTED
6150
) {
6251
val intent = Intent(action).apply {
6352
putExtra(DEVICE_NAME, deviceName)
6453
}
65-
bluetoothHeadsetReceiver.onReceive(context, intent)
54+
headsetManager.onReceive(context, intent)
6655
}
6756

6857
fun getTargetContext(): Context = getInstrumentation().targetContext

audioswitch/src/main/java/com/twilio/audioswitch/android/LogWrapper.kt

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ internal class LogWrapper {
88
Log.d(tag, message)
99
}
1010

11+
fun w(tag: String, message: String) {
12+
Log.w(tag, message)
13+
}
14+
1115
fun e(tag: String, message: String) {
1216
Log.e(tag, message)
1317
}

audioswitch/src/main/java/com/twilio/audioswitch/bluetooth/BluetoothController.kt

-49
This file was deleted.

audioswitch/src/main/java/com/twilio/audioswitch/bluetooth/BluetoothHeadsetCacheManager.kt

-28
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.twilio.audioswitch.bluetooth
22

33
internal interface BluetoothHeadsetConnectionListener {
4-
fun onBluetoothHeadsetStateChanged()
4+
fun onBluetoothHeadsetStateChanged(headsetName: String? = null)
5+
fun onBluetoothHeadsetActivationError()
56
}

0 commit comments

Comments
 (0)