Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 21bf81a0 authored by Etienne Ruffieux's avatar Etienne Ruffieux Committed by Qasim Javed
Browse files

Added Bluetooth audio device fallback on disconnect

HeadsetService#removeActiveDevice and A2dpService#
removeActiveDevice will now check if the device being
removed is not connected anymore. If so, and if another
device is connected to these profiles it will set the other
connected device active.

This will apply only when a disconnection happen on the
active device, other cases such as Hearing aid connection,
or wired connection are not impacted by this change.

In the case of A2dp, the music will not be paused when
switching from one device to another, but will still be
paused in other cases.

Bug: 202602952
Test: manually tested using Galaxy Buds pro and Sennheiser CX true
wireless buds
Tag: #feature
Ignore-AOSP-First: cherry-pick

Merged-In: Ia77d3d0520ff5f65779c9ea15ba0c1aa7eb1476b
Change-Id: Ia77d3d0520ff5f65779c9ea15ba0c1aa7eb1476b
parent c1912d22
Loading
Loading
Loading
Loading
+25 −1
Original line number Diff line number Diff line
@@ -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);
@@ -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));
@@ -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.
     */
+4 −2
Original line number Diff line number Diff line
@@ -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);
                        }
                    }
@@ -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);
                        }
                    }
+32 −0
Original line number Diff line number Diff line
@@ -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
     *
+20 −0
Original line number Diff line number Diff line
@@ -1359,6 +1359,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)) {
@@ -2149,6 +2159,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();
+39 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;

@@ -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 {
@@ -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);
@@ -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();
@@ -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
@@ -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.
     */
@@ -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.
     */