Telecomm changes to support dual mode Bluetooth audio devices
Bug: 265077663
Test: Manual
Change-Id: I4d2212178ffced79b812f4e808b8331ae0fe689e
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 4482ec6..303f41c 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -28,7 +28,9 @@
import android.media.AudioManager;
import android.media.AudioDeviceInfo;
import android.media.audio.common.AudioDevice;
+import android.os.Bundle;
import android.telecom.Log;
+import android.util.ArraySet;
import android.util.LocalLog;
import com.android.internal.util.IndentingPrintWriter;
@@ -238,18 +240,38 @@
}
public int getNumConnectedDevices() {
- synchronized (mLock) {
- return mHfpDevicesByAddress.size() +
- mHearingAidDevicesByAddress.size() +
- getLeAudioConnectedDevices().size();
- }
+ return getConnectedDevices().size();
}
public Collection<BluetoothDevice> getConnectedDevices() {
synchronized (mLock) {
- ArrayList<BluetoothDevice> result = new ArrayList<>(mHfpDevicesByAddress.values());
+ ArraySet<BluetoothDevice> result = new ArraySet<>();
+
+ // Set storing the group ids of all dual mode audio devices to de-dupe them
+ Set<Integer> dualModeGroupIds = new ArraySet<>();
+ for (BluetoothDevice hfpDevice: mHfpDevicesByAddress.values()) {
+ result.add(hfpDevice);
+ if (mBluetoothLeAudioService == null) {
+ continue;
+ }
+ int groupId = mBluetoothLeAudioService.getGroupId(hfpDevice);
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+ dualModeGroupIds.add(groupId);
+ }
+ }
+
result.addAll(mHearingAidDevicesByAddress.values());
- result.addAll(getLeAudioConnectedDevices());
+ if (mBluetoothLeAudioService == null) {
+ return Collections.unmodifiableCollection(result);
+ }
+ for (BluetoothDevice leAudioDevice: getLeAudioConnectedDevices()) {
+ // Exclude dual mode audio devices included from the HFP devices list
+ int groupId = mBluetoothLeAudioService.getGroupId(leAudioDevice);
+ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID
+ && !dualModeGroupIds.contains(groupId)) {
+ result.add(leAudioDevice);
+ }
+ }
return Collections.unmodifiableCollection(result);
}
}
@@ -257,9 +279,9 @@
// Same as getConnectedDevices except it filters out the hearing aid devices that are linked
// together by their hiSyncId.
public Collection<BluetoothDevice> getUniqueConnectedDevices() {
- ArrayList<BluetoothDevice> result;
+ ArraySet<BluetoothDevice> result;
synchronized (mLock) {
- result = new ArrayList<>(mHfpDevicesByAddress.values());
+ result = new ArraySet<>(mHfpDevicesByAddress.values());
}
Set<Long> seenHiSyncIds = new LinkedHashSet<>();
// Add the left-most active device to the seen list so that we match up with the list
@@ -371,6 +393,8 @@
return;
}
if (!targetDeviceMap.containsKey(device.getAddress())) {
+ Log.i(this, "Adding device with address: " + device + " and devicetype="
+ + getDeviceTypeString(deviceType));
targetDeviceMap.put(device.getAddress(), device);
mBluetoothRouteManager.onDeviceAdded(device.getAddress());
}
@@ -395,6 +419,8 @@
return;
}
if (targetDeviceMap.containsKey(device.getAddress())) {
+ Log.i(this, "Removing device with address: " + device + " and devicetype="
+ + getDeviceTypeString(deviceType));
targetDeviceMap.remove(device.getAddress());
mBluetoothRouteManager.onDeviceLost(device.getAddress());
}
@@ -567,15 +593,51 @@
// Connect audio to the bluetooth device at address, checking to see whether it's
// le audio, hearing aid or a HFP device, and using the proper BT API.
public boolean connectAudio(String address, boolean switchingBtDevices) {
+ int callProfile = BluetoothProfile.LE_AUDIO;
+ Log.i(this, "Telecomm connecting audio to device: " + address);
+ BluetoothDevice device = null;
if (mLeAudioDevicesByAddress.containsKey(address)) {
+ Log.i(this, "Telecomm found LE Audio device for address: " + address);
if (mBluetoothLeAudioService == null) {
Log.w(this, "Attempting to turn on audio when the le audio service is null");
return false;
}
- BluetoothDevice device = mLeAudioDevicesByAddress.get(address);
+ device = mLeAudioDevicesByAddress.get(address);
+ callProfile = BluetoothProfile.LE_AUDIO;
+ } else if (mHearingAidDevicesByAddress.containsKey(address)) {
+ Log.i(this, "Telecomm found hearing aid device for address: " + address);
+ if (mBluetoothHearingAid == null) {
+ Log.w(this, "Attempting to turn on audio when the hearing aid service is null");
+ return false;
+ }
+ device = mHearingAidDevicesByAddress.get(address);
+ callProfile = BluetoothProfile.HEARING_AID;
+ } else if (mHfpDevicesByAddress.containsKey(address)) {
+ Log.i(this, "Telecomm found HFP device for address: " + address);
+ if (mBluetoothHeadset == null) {
+ Log.w(this, "Attempting to turn on audio when the headset service is null");
+ return false;
+ }
+ device = mHfpDevicesByAddress.get(address);
+ callProfile = BluetoothProfile.HEADSET;
+ }
+
+ if (device == null) {
+ Log.w(this, "No active profiles for Bluetooth address=" + address);
+ return false;
+ }
+
+ Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device);
+ if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+ && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) {
+ Log.i(this, "Preferred duplex profile for device=" + address + " is "
+ + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX));
+ callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+ }
+
+ if (callProfile == BluetoothProfile.LE_AUDIO) {
if (mBluetoothAdapter.setActiveDevice(
device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
-
/* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
* Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
* will be audio switched to is available to be choose as communication device */
@@ -583,19 +645,10 @@
return mCommunicationDeviceTracker.setCommunicationDevice(
AudioDeviceInfo.TYPE_BLE_HEADSET, device);
}
-
- return true;
}
return false;
- } else if (mHearingAidDevicesByAddress.containsKey(address)) {
- if (mBluetoothHearingAid == null) {
- Log.w(this, "Attempting to turn on audio when the hearing aid service is null");
- return false;
- }
- if (mBluetoothAdapter.setActiveDevice(
- mHearingAidDevicesByAddress.get(address),
- BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
-
+ } else if (callProfile == BluetoothProfile.HEARING_AID) {
+ if (mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) {
/* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device.
* Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that
* will be audio switched to is available to be choose as communication device */
@@ -603,16 +656,9 @@
return mCommunicationDeviceTracker.setCommunicationDevice(
AudioDeviceInfo.TYPE_HEARING_AID, null);
}
-
- return true;
}
return false;
- } else if (mHfpDevicesByAddress.containsKey(address)) {
- BluetoothDevice device = mHfpDevicesByAddress.get(address);
- if (mBluetoothHeadset == null) {
- Log.w(this, "Attempting to turn on audio when the headset service is null");
- return false;
- }
+ } else if (callProfile == BluetoothProfile.HEADSET) {
boolean success = mBluetoothAdapter.setActiveDevice(device,
BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
if (!success) {
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index ab002c9..a447c69e 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -16,6 +16,7 @@
package com.android.server.telecom.bluetooth;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
@@ -26,6 +27,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceInfo;
+import android.os.Bundle;
import android.telecom.Log;
import android.telecom.Logging.Session;
@@ -86,7 +88,7 @@
intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
BluetoothDevice device =
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
if (device == null) {
Log.w(LOG_TAG, "Got null device from broadcast. " +
"Ignoring.");
@@ -117,7 +119,7 @@
int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_DISCONNECTED);
BluetoothDevice device =
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
if (device == null) {
Log.w(LOG_TAG, "Got null device from broadcast. " +
@@ -151,7 +153,7 @@
private void handleActiveDeviceChanged(Intent intent) {
BluetoothDevice device =
- intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
int deviceType;
if (BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
@@ -183,11 +185,31 @@
}
args.arg2 = device.getAddress();
+ boolean usePreferredAudioProfile = false;
+ BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager.getBluetoothAdapter();
+ int preferredDuplexProfile = BluetoothProfile.LE_AUDIO;
+ if (bluetoothAdapter != null) {
+ Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles(
+ device);
+ if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty()
+ && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)
+ != 0) {
+ Log.i(this, "Preferred duplex profile for device=" + device + " is "
+ + preferredAudioProfiles.getInt(
+ BluetoothAdapter.AUDIO_MODE_DUPLEX));
+ usePreferredAudioProfile = true;
+ preferredDuplexProfile =
+ preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX);
+ }
+ }
+
if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
/* In Le Audio case, once device got Active, the Telecom needs to make sure it
* is set as communication device before we can say that BT_AUDIO_IS_ON
*/
- if (!mCommunicationDeviceTracker.setCommunicationDevice(
+ if ((!usePreferredAudioProfile
+ || preferredDuplexProfile == BluetoothProfile.LE_AUDIO)
+ && !mCommunicationDeviceTracker.setCommunicationDevice(
AudioDeviceInfo.TYPE_BLE_HEADSET, device)) {
Log.w(LOG_TAG,
"Device %s cannot be use as LE audio communication device.",
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
index 344469d..de57984 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothDeviceManagerTest.java
@@ -27,6 +27,7 @@
import android.content.Intent;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.os.Bundle;
import android.os.Parcel;
import android.test.suitebuilder.annotation.SmallTest;
@@ -53,6 +54,7 @@
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -182,6 +184,7 @@
buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+ when(mBluetoothLeAudio.getGroupId(device5)).thenReturn(1);
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device5);
receiverUnderTest.onReceive(mContext,
@@ -192,6 +195,7 @@
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 2);
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(2)).thenReturn(device6);
+ when(mBluetoothLeAudio.getGroupId(device6)).thenReturn(1);
receiverUnderTest.onReceive(mContext,
buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device3,
BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
@@ -267,17 +271,19 @@
@Test
public void testLeAudioDedup() {
receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device1,
+ buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device1,
BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device5,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
receiverUnderTest.onReceive(mContext,
- buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device6,
+ buildConnectionActionIntent(BluetoothProfile.STATE_CONNECTED, device6,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 1);
when(mBluetoothLeAudio.getConnectedGroupLeadDevice(1)).thenReturn(device5);
+ when(mBluetoothLeAudio.getGroupId(device5)).thenReturn(1);
+ when(mBluetoothLeAudio.getGroupId(device6)).thenReturn(1);
assertEquals(2, mBluetoothDeviceManager.getNumConnectedDevices());
assertEquals(2, mBluetoothDeviceManager.getUniqueConnectedDevices().size());
}
@@ -462,6 +468,8 @@
verify(mBluetoothHeadset, never()).connectAudio();
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_AUDIO));
receiverUnderTest.onReceive(mContext, buildActiveDeviceChangeActionIntent(device5,
BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
@@ -489,6 +497,8 @@
verify(mBluetoothHeadset, never()).connectAudio();
verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
when(mAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
.thenReturn(Arrays.asList(device5, device6));
@@ -585,6 +595,100 @@
@SmallTest
@Test
+ public void testConnectDualModeEarbud() {
+ receiverUnderTest.setIsInCall(true);
+
+ // LE Audio earbuds connected
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothLeAudio.STATE_CONNECTED, device6,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device6, 1);
+ // HFP device connected
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_HEADSET));
+ when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
+
+ AudioDeviceInfo mockAudioDevice5Info = createMockAudioDeviceInfo(device5.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
+ AudioDeviceInfo mockAudioDevice6Info = createMockAudioDeviceInfo(device6.getAddress(),
+ AudioDeviceInfo.TYPE_BLE_HEADSET);
+ when(mockAudioDevice5Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+ when(mockAudioDevice6Info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET);
+ List<AudioDeviceInfo> devices = new ArrayList<>();
+ devices.add(mockAudioDevice5Info);
+ devices.add(mockAudioDevice6Info);
+
+ when(mockAudioManager.getAvailableCommunicationDevices())
+ .thenReturn(devices);
+ when(mockAudioManager.setCommunicationDevice(mockAudioDevice5Info))
+ .thenReturn(true);
+
+ Bundle hfpPreferred = new Bundle();
+ hfpPreferred.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.HEADSET);
+ Bundle leAudioPreferred = new Bundle();
+ leAudioPreferred.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.LE_AUDIO);
+
+ // TEST 1: LE Audio preferred for DUPLEX
+ when(mAdapter.getPreferredAudioProfiles(device5)).thenReturn(leAudioPreferred);
+ when(mAdapter.getPreferredAudioProfiles(device6)).thenReturn(leAudioPreferred);
+ mBluetoothDeviceManager.connectAudio(device5.getAddress(), false);
+ verify(mAdapter, times(1)).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ verify(mBluetoothHeadset, never()).connectAudio();
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mockAudioManager).setCommunicationDevice(mockAudioDevice5Info);
+
+ when(mAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
+ .thenReturn(Arrays.asList(device5, device6));
+
+ // Check disconnect during a call
+ devices.remove(mockAudioDevice5Info);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_DISCONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeRemoved(device5, 1);
+
+ mBluetoothDeviceManager.connectAudio(device6.getAddress(), false);
+ verify(mAdapter).setActiveDevice(device6, BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ verify(mBluetoothHeadset, never()).connectAudio();
+ verify(mAdapter, never()).setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL));
+ verify(mockAudioManager, times(1)).clearCommunicationDevice();
+
+ // Reconnect other LE Audio earbud
+ devices.add(mockAudioDevice5Info);
+ receiverUnderTest.onReceive(mContext,
+ buildConnectionActionIntent(BluetoothHeadset.STATE_CONNECTED, device5,
+ BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO));
+ leAudioCallbacksTest.getValue().onGroupNodeAdded(device5, 1);
+
+ // Disconnects audio
+ mBluetoothDeviceManager.disconnectAudio();
+ verify(mockAudioManager, times(2)).clearCommunicationDevice();
+ verify(mBluetoothHeadset, times(1)).disconnectAudio();
+
+ // TEST 2: HFP preferred for DUPLEX
+ when(mAdapter.getPreferredAudioProfiles(device5)).thenReturn(hfpPreferred);
+ when(mAdapter.getPreferredAudioProfiles(device6)).thenReturn(hfpPreferred);
+ when(mAdapter.setActiveDevice(nullable(BluetoothDevice.class),
+ eq(BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL))).thenReturn(true);
+ mBluetoothDeviceManager.connectAudio(device5.getAddress(), false);
+ verify(mAdapter).setActiveDevice(device5, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL);
+ verify(mAdapter, times(1)).setActiveDevice(device5,
+ BluetoothAdapter.ACTIVE_DEVICE_ALL);
+ verify(mBluetoothHeadset).connectAudio();
+ mBluetoothDeviceManager.disconnectAudio();
+ verify(mBluetoothHeadset, times(2)).disconnectAudio();
+ }
+
+ @SmallTest
+ @Test
public void testClearHearingAidCommunicationDevice() {
AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
when(mockAudioDeviceInfo.getAddress()).thenReturn(DEVICE_ADDRESS_1);