Activate the last connected BT device
When BT1 connected -> BT2 connected -> BT2 disconnected,
then the sound came from phone speaker before this CL.
With the CL, now the sound comes from BT1.
Bug: 243053736
Tag: #refactor
Test: atest BluetoothInstrumentationTests:ActiveDeviceManagerTest
Change-Id: I1d019bfd117f7e9f13618dfa989c331b33be3f42
(cherry picked from commit 225fef2717d6daa7e027447939d22c17da2339ea)
Merged-In: I1d019bfd117f7e9f13618dfa989c331b33be3f42
diff --git a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java
index 313f64c..5c160ac 100644
--- a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java
+++ b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java
@@ -1292,7 +1292,7 @@
/**
* Retrieves the most recently connected device in the A2DP connected devices list.
*/
- private BluetoothDevice getFallbackDevice() {
+ public BluetoothDevice getFallbackDevice() {
DatabaseManager dbManager = mAdapterService.getDatabase();
return dbManager != null ? dbManager
.getMostRecentlyConnectedDevicesInList(getConnectedDevices())
diff --git a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
index 1f63a88..ec3f4f4 100644
--- a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
+++ b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
@@ -40,6 +40,7 @@
import android.util.Log;
import com.android.bluetooth.a2dp.A2dpService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
import com.android.bluetooth.le_audio.LeAudioService;
@@ -51,8 +52,10 @@
/**
* The active device manager is responsible for keeping track of the
- * connected A2DP/HFP/AVRCP/HearingAid devices and select which device is
+ * connected A2DP/HFP/AVRCP/HearingAid/LE audio devices and select which device is
* active (for each profile).
+ * The active device manager selects a fallback device when the currently active device
+ * is disconnected, and it selects BT devices that are lastly activated one.
*
* Current policy (subject to change):
* 1) If the maximum number of connected devices is one, the manager doesn't
@@ -66,24 +69,24 @@
* - BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED for A2DP
* - BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED for HFP
* - BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED for HearingAid
+ * - BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED for LE audio
* If such broadcast is received (e.g., triggered indirectly by user
- * action on the UI), the device in the received broacast is marked
+ * action on the UI), the device in the received broadcast is marked
* as the current active device for that profile.
- * 5) If there is a HearingAid active device, then A2DP and HFP active devices
- * must be set to null (i.e., A2DP and HFP cannot have active devices).
- * The reason is because A2DP or HFP cannot be used together with HearingAid.
+ * 5) If there is a HearingAid active device, then A2DP, HFP and LE audio active devices
+ * must be set to null (i.e., A2DP, HFP and LE audio cannot have active devices).
+ * The reason is that A2DP, HFP or LE audio cannot be used together with HearingAid.
* 6) If there are no connected devices (e.g., during startup, or after all
* devices have been disconnected, the active device per profile
- * (A2DP/HFP/HearingAid) is selected as follows:
+ * (A2DP/HFP/HearingAid/LE audio) is selected as follows:
* 6.1) The last connected HearingAid device is selected as active.
- * If there is an active A2DP or HFP device, those must be set to null.
- * 6.2) The last connected A2DP or HFP device is selected as active.
+ * If there is an active A2DP, HFP or LE audio device, those must be set to null.
+ * 6.2) The last connected A2DP, HFP or LE audio device is selected as active.
* However, if there is an active HearingAid device, then the
- * A2DP or HFP active device is not set (must remain null).
+ * A2DP, HFP, or LE audio active device is not set (must remain null).
* 7) If the currently active device (per profile) is disconnected, the
* Active Device Manager just marks that the profile has no active device,
- * but does not attempt to select a new one. Currently, the expectation is
- * that the user will explicitly select the new active device.
+ * and the lastly activated BT device that is still connected would be selected.
* 8) If there is already an active device, and the corresponding
* ACTION_ACTIVE_DEVICE_CHANGED broadcast is received, the device
* contained in the broadcast is marked as active. However, if
@@ -91,7 +94,7 @@
* as having no active device.
* 9) If a wired audio device is connected, the audio output is switched
* by the Audio Framework itself to that device. We detect this here,
- * and the active device for each profile (A2DP/HFP/HearingAid) is set
+ * and the active device for each profile (A2DP/HFP/HearingAid/LE audio) is set
* to null to reflect the output device state change. However, if the
* wired audio device is disconnected, we don't do anything explicit
* and apply the default behavior instead:
@@ -252,9 +255,11 @@
+ "device " + device + " disconnected");
}
mA2dpConnectedDevices.remove(device);
- if (mA2dpConnectedDevices.isEmpty()
- && Objects.equals(mA2dpActiveDevice, device)) {
- setA2dpActiveDevice(null);
+ if (Objects.equals(mA2dpActiveDevice, device)) {
+ if (mA2dpConnectedDevices.isEmpty()) {
+ setA2dpActiveDevice(null);
+ }
+ setFallbackDeviceActive();
}
}
}
@@ -314,9 +319,11 @@
+ "device " + device + " disconnected");
}
mHfpConnectedDevices.remove(device);
- if (mHfpConnectedDevices.isEmpty()
- && Objects.equals(mHfpActiveDevice, device)) {
- setHfpActiveDevice(null);
+ if (Objects.equals(mHfpActiveDevice, device)) {
+ if (mHfpConnectedDevices.isEmpty()) {
+ setHfpActiveDevice(null);
+ }
+ setFallbackDeviceActive();
}
}
}
@@ -392,9 +399,11 @@
+ "_CHANGED): device " + device + " disconnected");
}
mLeAudioConnectedDevices.remove(device);
- if (mLeAudioConnectedDevices.isEmpty()
- && Objects.equals(mLeAudioActiveDevice, device)) {
- setLeAudioActiveDevice(null);
+ if (Objects.equals(mLeAudioActiveDevice, device)) {
+ if (mLeAudioConnectedDevices.isEmpty()) {
+ setLeAudioActiveDevice(null);
+ }
+ setFallbackDeviceActive();
}
}
}
@@ -658,6 +667,87 @@
}
}
+ private void setFallbackDeviceActive() {
+ if (DBG) {
+ Log.d(TAG, "setFallbackDeviceActive");
+ }
+ DatabaseManager dbManager = mAdapterService.getDatabase();
+ if (dbManager == null) {
+ return;
+ }
+
+ A2dpService a2dpService = mFactory.getA2dpService();
+ BluetoothDevice a2dpFallbackDevice = null;
+ if (a2dpService != null) {
+ a2dpFallbackDevice = a2dpService.getFallbackDevice();
+ }
+
+ HeadsetService headsetService = mFactory.getHeadsetService();
+ BluetoothDevice headsetFallbackDevice = null;
+ if (headsetService != null) {
+ headsetFallbackDevice = headsetService.getFallbackDevice();
+ }
+
+ List<BluetoothDevice> connectedDevices = new LinkedList<>();
+ connectedDevices.addAll(mLeAudioConnectedDevices);
+ switch (mAudioManager.getMode()) {
+ case AudioManager.MODE_NORMAL:
+ if (a2dpFallbackDevice != null) {
+ connectedDevices.add(a2dpFallbackDevice);
+ }
+ break;
+ case AudioManager.MODE_RINGTONE:
+ if (headsetFallbackDevice != null && headsetService.isInbandRingingEnabled()) {
+ connectedDevices.add(headsetFallbackDevice);
+ }
+ break;
+ default:
+ if (headsetFallbackDevice != null) {
+ connectedDevices.add(headsetFallbackDevice);
+ }
+ }
+ BluetoothDevice device = dbManager.getMostRecentlyConnectedDevicesInList(connectedDevices);
+ if (device != null) {
+ if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) {
+ if (Objects.equals(a2dpFallbackDevice, device)) {
+ if (DBG) {
+ Log.d(TAG, "set A2DP device active: " + device);
+ }
+ setA2dpActiveDevice(device);
+ if (headsetFallbackDevice != null) {
+ setHfpActiveDevice(device);
+ setLeAudioActiveDevice(null);
+ }
+ } else {
+ if (DBG) {
+ Log.d(TAG, "set LE audio device active: " + device);
+ }
+ setLeAudioActiveDevice(device);
+ setA2dpActiveDevice(null);
+ setHfpActiveDevice(null);
+ }
+ } else {
+ if (Objects.equals(headsetFallbackDevice, device)) {
+ if (DBG) {
+ Log.d(TAG, "set HFP device active: " + device);
+ }
+ setHfpActiveDevice(device);
+ if (a2dpFallbackDevice != null) {
+ setA2dpActiveDevice(a2dpFallbackDevice);
+ setLeAudioActiveDevice(null);
+ }
+ } else {
+ if (DBG) {
+ Log.d(TAG, "set LE audio device active: " + device);
+ }
+ setLeAudioActiveDevice(device);
+ setA2dpActiveDevice(null);
+ setHfpActiveDevice(null);
+ }
+ }
+ }
+ }
+
private void resetState() {
mA2dpConnectedDevices.clear();
mA2dpActiveDevice = null;
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
index 275ae63..a951616 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -1927,7 +1927,12 @@
return true;
}
- boolean isInbandRingingEnabled() {
+ /**
+ * Checks if headset devices are able to get inband ringing.
+ *
+ * @return True if inband ringing is enabled.
+ */
+ public boolean isInbandRingingEnabled() {
boolean isInbandRingingSupported = getResources().getBoolean(
com.android.bluetooth.R.bool.config_bluetooth_hfp_inband_ringing_support);
return isInbandRingingSupported && !SystemProperties.getBoolean(
@@ -2176,7 +2181,7 @@
/**
* Retrieves the most recently connected device in the A2DP connected devices list.
*/
- private BluetoothDevice getFallbackDevice() {
+ public BluetoothDevice getFallbackDevice() {
DatabaseManager dbManager = mAdapterService.getDatabase();
return dbManager != null ? dbManager
.getMostRecentlyConnectedDevicesInList(getConnectedDevices())
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
index 28e68cf..f182375 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
@@ -50,6 +50,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+
@MediumTest
@RunWith(AndroidJUnit4.class)
public class ActiveDeviceManagerTest {
@@ -474,6 +476,56 @@
}
/**
+ * An LE Audio connected. An A2DP connected. The A2DP disconnected.
+ * Then the LE Audio should be the active one.
+ */
+ @Test
+ public void leAudioAndA2dpConnectedThenA2dpDisconnected_fallbackToLeAudio() {
+ leAudioConnected(mLeAudioDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
+
+ a2dpConnected(mA2dpDevice);
+ verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
+
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
+ when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer(
+ invocation -> {
+ List<BluetoothDevice> devices = invocation.getArgument(0);
+ return (devices != null && devices.contains(mLeAudioDevice))
+ ? mLeAudioDevice : null;
+ }
+ );
+ a2dpDisconnected(mA2dpDevice);
+ verify(mA2dpService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull());
+ verify(mLeAudioService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mLeAudioDevice);
+ }
+
+ /**
+ * An A2DP connected. An LE Audio connected. The LE Audio disconnected.
+ * Then the A2DP should be the active one.
+ */
+ @Test
+ public void a2dpAndLeAudioConnectedThenLeAudioDisconnected_fallbackToA2dp() {
+ a2dpConnected(mA2dpDevice);
+ verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
+
+ leAudioConnected(mLeAudioDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
+
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
+ when(mA2dpService.getFallbackDevice()).thenReturn(mA2dpDevice);
+ when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer(
+ invocation -> {
+ List<BluetoothDevice> devices = invocation.getArgument(0);
+ return (devices != null && devices.contains(mA2dpDevice)) ? mA2dpDevice : null;
+ }
+ );
+ leAudioDisconnected(mLeAudioDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull());
+ verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mA2dpDevice);
+ }
+
+ /**
* One LE Hearing Aid is connected.
*/
@Test