Loading android/app/src/com/android/bluetooth/a2dp/A2dpService.java +25 −1 Original line number Diff line number Diff line Loading @@ -490,6 +490,20 @@ public class A2dpService extends ProfileService { if (mActiveDevice == null) return; previousActiveDevice = mActiveDevice; } int prevActiveConnectionState = getConnectionState(previousActiveDevice); // As per b/202602952, if we remove the active device due to a disconnection, // we need to check if another device is connected and set it active instead. // Calling this before any other active related calls has the same effect as // a classic active device switch. BluetoothDevice fallbackdevice = getFallbackDevice(); if (fallbackdevice != null && prevActiveConnectionState != BluetoothProfile.STATE_CONNECTED) { setActiveDevice(fallbackdevice); return; } // This needs to happen before we inform the audio manager that the device // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why. updateAndBroadcastActiveDevice(null); Loading @@ -499,7 +513,7 @@ public class A2dpService extends ProfileService { // device, the user has explicitly switched the output to the local device and music // should continue playing. Otherwise, the remote device has been indeed disconnected // and audio should be suspended before switching the output to the local device. boolean stopAudio = forceStopPlayingAudio || (getConnectionState(previousActiveDevice) boolean stopAudio = forceStopPlayingAudio || (prevActiveConnectionState != BluetoothProfile.STATE_CONNECTED); mAudioManager.handleBluetoothActiveDeviceChanged(null, previousActiveDevice, BluetoothProfileConnectionInfo.createA2dpInfo(!stopAudio, -1)); Loading Loading @@ -1241,6 +1255,16 @@ public class A2dpService extends ProfileService { } } /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) : null; } /** * Binder object: must be a static class or memory leak may occur. */ Loading android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java +4 −2 Original line number Diff line number Diff line Loading @@ -233,7 +233,8 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mA2dpConnectedDevices.remove(device); if (Objects.equals(mA2dpActiveDevice, device)) { if (mA2dpConnectedDevices.isEmpty() && Objects.equals(mA2dpActiveDevice, device)) { setA2dpActiveDevice(null); } } Loading Loading @@ -294,7 +295,8 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mHfpConnectedDevices.remove(device); if (Objects.equals(mHfpActiveDevice, device)) { if (mHfpConnectedDevices.isEmpty() && Objects.equals(mHfpActiveDevice, device)) { setHfpActiveDevice(null); } } Loading android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java +32 −0 Original line number Diff line number Diff line Loading @@ -637,6 +637,38 @@ public class DatabaseManager { return mostRecentlyConnectedDevices; } /** * Gets the most recently connected bluetooth device in a given list. * * @param devicesList the list of {@link BluetoothDevice} to search in * @return the most recently connected {@link BluetoothDevice} in the given * {@code devicesList}, or null if an error occurred * * @hide */ public BluetoothDevice getMostRecentlyConnectedDevicesInList( List<BluetoothDevice> devicesList) { if (devicesList == null) { return null; } BluetoothDevice mostRecentDevice = null; long mostRecentLastActiveTime = -1; synchronized (mMetadataCache) { for (BluetoothDevice device : devicesList) { String address = device.getAddress(); Metadata metadata = mMetadataCache.get(address); if (metadata != null && (mostRecentLastActiveTime == -1 || mostRecentLastActiveTime < metadata.last_active_time)) { mostRecentLastActiveTime = metadata.last_active_time; mostRecentDevice = device; } } } return mostRecentDevice; } /** * Gets the last active a2dp device * Loading android/app/src/com/android/bluetooth/hfp/HeadsetService.java +20 −0 Original line number Diff line number Diff line Loading @@ -1363,6 +1363,16 @@ public class HeadsetService extends ProfileService { */ private void removeActiveDevice() { synchronized (mStateMachines) { // As per b/202602952, if we remove the active device due to a disconnection, // we need to check if another device is connected and set it active instead. // Calling this before any other active related calls has the same effect as // a classic active device switch. BluetoothDevice fallbackDevice = getFallbackDevice(); if (fallbackDevice != null && mActiveDevice != null && getConnectionState(mActiveDevice) != BluetoothProfile.STATE_CONNECTED) { setActiveDevice(fallbackDevice); return; } // Clear the active device if (mVoiceRecognitionStarted) { if (!stopVoiceRecognition(mActiveDevice)) { Loading Loading @@ -2162,6 +2172,16 @@ public class HeadsetService extends ProfileService { == mStateMachinesThread.getId()); } /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) : null; } @Override public void dump(StringBuilder sb) { boolean isScoOn = mSystemInterface.getAudioManager().isBluetoothScoOn(); Loading android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.TestUtils; 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; Loading @@ -58,6 +59,7 @@ public class ActiveDeviceManagerTest { private BluetoothDevice mA2dpHeadsetDevice; private BluetoothDevice mHearingAidDevice; private BluetoothDevice mLeAudioDevice; private BluetoothDevice mSecondaryAudioDevice; private ActiveDeviceManager mActiveDeviceManager; private static final int TIMEOUT_MS = 1000; Loading @@ -68,6 +70,7 @@ public class ActiveDeviceManagerTest { @Mock private HearingAidService mHearingAidService; @Mock private LeAudioService mLeAudioService; @Mock private AudioManager mAudioManager; @Mock private DatabaseManager mDatabaseManager; @Before public void setUp() throws Exception { Loading @@ -82,6 +85,7 @@ public class ActiveDeviceManagerTest { when(mAdapterService.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager); when(mAdapterService.getSystemServiceName(AudioManager.class)) .thenReturn(Context.AUDIO_SERVICE); when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); when(mServiceFactory.getA2dpService()).thenReturn(mA2dpService); when(mServiceFactory.getHeadsetService()).thenReturn(mHeadsetService); when(mServiceFactory.getHearingAidService()).thenReturn(mHearingAidService); Loading @@ -90,6 +94,8 @@ public class ActiveDeviceManagerTest { when(mHeadsetService.setActiveDevice(any())).thenReturn(true); when(mHearingAidService.setActiveDevice(any())).thenReturn(true); when(mLeAudioService.setActiveDevice(any())).thenReturn(true); when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())) .thenReturn(mSecondaryAudioDevice); mActiveDeviceManager = new ActiveDeviceManager(mAdapterService, mServiceFactory); mActiveDeviceManager.start(); Loading @@ -101,6 +107,7 @@ public class ActiveDeviceManagerTest { mA2dpHeadsetDevice = TestUtils.getTestDevice(mAdapter, 2); mHearingAidDevice = TestUtils.getTestDevice(mAdapter, 3); mLeAudioDevice = TestUtils.getTestDevice(mAdapter, 4); mSecondaryAudioDevice = TestUtils.getTestDevice(mAdapter, 4); } @After Loading Loading @@ -166,6 +173,22 @@ public class ActiveDeviceManagerTest { Assert.assertEquals(mA2dpDevice, mActiveDeviceManager.getA2dpActiveDevice()); } /** * Two A2DP devices are connected and the current active is then disconnected. * Should then set active device to fallback device. */ @Test public void a2dpSecondDeviceDisconnected_fallbackDeviceActive() { a2dpConnected(mSecondaryAudioDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); a2dpConnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice); a2dpDisconnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); } /** * One Headset is connected. */ Loading Loading @@ -218,6 +241,22 @@ public class ActiveDeviceManagerTest { } /** * Two Headsets are connected and the current active is then disconnected. * Should then set active device to fallback device. */ @Test public void headsetSecondDeviceDisconnected_fallbackDeviceActive() { headsetConnected(mSecondaryAudioDevice); verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); headsetConnected(mHeadsetDevice); verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice); headsetDisconnected(mHeadsetDevice); verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); } /** * A combo (A2DP + Headset) device is connected. Then a Hearing Aid is connected. */ Loading Loading
android/app/src/com/android/bluetooth/a2dp/A2dpService.java +25 −1 Original line number Diff line number Diff line Loading @@ -490,6 +490,20 @@ public class A2dpService extends ProfileService { if (mActiveDevice == null) return; previousActiveDevice = mActiveDevice; } int prevActiveConnectionState = getConnectionState(previousActiveDevice); // As per b/202602952, if we remove the active device due to a disconnection, // we need to check if another device is connected and set it active instead. // Calling this before any other active related calls has the same effect as // a classic active device switch. BluetoothDevice fallbackdevice = getFallbackDevice(); if (fallbackdevice != null && prevActiveConnectionState != BluetoothProfile.STATE_CONNECTED) { setActiveDevice(fallbackdevice); return; } // This needs to happen before we inform the audio manager that the device // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why. updateAndBroadcastActiveDevice(null); Loading @@ -499,7 +513,7 @@ public class A2dpService extends ProfileService { // device, the user has explicitly switched the output to the local device and music // should continue playing. Otherwise, the remote device has been indeed disconnected // and audio should be suspended before switching the output to the local device. boolean stopAudio = forceStopPlayingAudio || (getConnectionState(previousActiveDevice) boolean stopAudio = forceStopPlayingAudio || (prevActiveConnectionState != BluetoothProfile.STATE_CONNECTED); mAudioManager.handleBluetoothActiveDeviceChanged(null, previousActiveDevice, BluetoothProfileConnectionInfo.createA2dpInfo(!stopAudio, -1)); Loading Loading @@ -1241,6 +1255,16 @@ public class A2dpService extends ProfileService { } } /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) : null; } /** * Binder object: must be a static class or memory leak may occur. */ Loading
android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java +4 −2 Original line number Diff line number Diff line Loading @@ -233,7 +233,8 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mA2dpConnectedDevices.remove(device); if (Objects.equals(mA2dpActiveDevice, device)) { if (mA2dpConnectedDevices.isEmpty() && Objects.equals(mA2dpActiveDevice, device)) { setA2dpActiveDevice(null); } } Loading Loading @@ -294,7 +295,8 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mHfpConnectedDevices.remove(device); if (Objects.equals(mHfpActiveDevice, device)) { if (mHfpConnectedDevices.isEmpty() && Objects.equals(mHfpActiveDevice, device)) { setHfpActiveDevice(null); } } Loading
android/app/src/com/android/bluetooth/btservice/storage/DatabaseManager.java +32 −0 Original line number Diff line number Diff line Loading @@ -637,6 +637,38 @@ public class DatabaseManager { return mostRecentlyConnectedDevices; } /** * Gets the most recently connected bluetooth device in a given list. * * @param devicesList the list of {@link BluetoothDevice} to search in * @return the most recently connected {@link BluetoothDevice} in the given * {@code devicesList}, or null if an error occurred * * @hide */ public BluetoothDevice getMostRecentlyConnectedDevicesInList( List<BluetoothDevice> devicesList) { if (devicesList == null) { return null; } BluetoothDevice mostRecentDevice = null; long mostRecentLastActiveTime = -1; synchronized (mMetadataCache) { for (BluetoothDevice device : devicesList) { String address = device.getAddress(); Metadata metadata = mMetadataCache.get(address); if (metadata != null && (mostRecentLastActiveTime == -1 || mostRecentLastActiveTime < metadata.last_active_time)) { mostRecentLastActiveTime = metadata.last_active_time; mostRecentDevice = device; } } } return mostRecentDevice; } /** * Gets the last active a2dp device * Loading
android/app/src/com/android/bluetooth/hfp/HeadsetService.java +20 −0 Original line number Diff line number Diff line Loading @@ -1363,6 +1363,16 @@ public class HeadsetService extends ProfileService { */ private void removeActiveDevice() { synchronized (mStateMachines) { // As per b/202602952, if we remove the active device due to a disconnection, // we need to check if another device is connected and set it active instead. // Calling this before any other active related calls has the same effect as // a classic active device switch. BluetoothDevice fallbackDevice = getFallbackDevice(); if (fallbackDevice != null && mActiveDevice != null && getConnectionState(mActiveDevice) != BluetoothProfile.STATE_CONNECTED) { setActiveDevice(fallbackDevice); return; } // Clear the active device if (mVoiceRecognitionStarted) { if (!stopVoiceRecognition(mActiveDevice)) { Loading Loading @@ -2162,6 +2172,16 @@ public class HeadsetService extends ProfileService { == mStateMachinesThread.getId()); } /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) : null; } @Override public void dump(StringBuilder sb) { boolean isScoOn = mSystemInterface.getAudioManager().isBluetoothScoOn(); Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.TestUtils; 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; Loading @@ -58,6 +59,7 @@ public class ActiveDeviceManagerTest { private BluetoothDevice mA2dpHeadsetDevice; private BluetoothDevice mHearingAidDevice; private BluetoothDevice mLeAudioDevice; private BluetoothDevice mSecondaryAudioDevice; private ActiveDeviceManager mActiveDeviceManager; private static final int TIMEOUT_MS = 1000; Loading @@ -68,6 +70,7 @@ public class ActiveDeviceManagerTest { @Mock private HearingAidService mHearingAidService; @Mock private LeAudioService mLeAudioService; @Mock private AudioManager mAudioManager; @Mock private DatabaseManager mDatabaseManager; @Before public void setUp() throws Exception { Loading @@ -82,6 +85,7 @@ public class ActiveDeviceManagerTest { when(mAdapterService.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager); when(mAdapterService.getSystemServiceName(AudioManager.class)) .thenReturn(Context.AUDIO_SERVICE); when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); when(mServiceFactory.getA2dpService()).thenReturn(mA2dpService); when(mServiceFactory.getHeadsetService()).thenReturn(mHeadsetService); when(mServiceFactory.getHearingAidService()).thenReturn(mHearingAidService); Loading @@ -90,6 +94,8 @@ public class ActiveDeviceManagerTest { when(mHeadsetService.setActiveDevice(any())).thenReturn(true); when(mHearingAidService.setActiveDevice(any())).thenReturn(true); when(mLeAudioService.setActiveDevice(any())).thenReturn(true); when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())) .thenReturn(mSecondaryAudioDevice); mActiveDeviceManager = new ActiveDeviceManager(mAdapterService, mServiceFactory); mActiveDeviceManager.start(); Loading @@ -101,6 +107,7 @@ public class ActiveDeviceManagerTest { mA2dpHeadsetDevice = TestUtils.getTestDevice(mAdapter, 2); mHearingAidDevice = TestUtils.getTestDevice(mAdapter, 3); mLeAudioDevice = TestUtils.getTestDevice(mAdapter, 4); mSecondaryAudioDevice = TestUtils.getTestDevice(mAdapter, 4); } @After Loading Loading @@ -166,6 +173,22 @@ public class ActiveDeviceManagerTest { Assert.assertEquals(mA2dpDevice, mActiveDeviceManager.getA2dpActiveDevice()); } /** * Two A2DP devices are connected and the current active is then disconnected. * Should then set active device to fallback device. */ @Test public void a2dpSecondDeviceDisconnected_fallbackDeviceActive() { a2dpConnected(mSecondaryAudioDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); a2dpConnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice); a2dpDisconnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); } /** * One Headset is connected. */ Loading Loading @@ -218,6 +241,22 @@ public class ActiveDeviceManagerTest { } /** * Two Headsets are connected and the current active is then disconnected. * Should then set active device to fallback device. */ @Test public void headsetSecondDeviceDisconnected_fallbackDeviceActive() { headsetConnected(mSecondaryAudioDevice); verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); headsetConnected(mHeadsetDevice); verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice); headsetDisconnected(mHeadsetDevice); verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice); } /** * A combo (A2DP + Headset) device is connected. Then a Hearing Aid is connected. */ Loading