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

Commit 788423e5 authored by Łukasz Rymanowski's avatar Łukasz Rymanowski
Browse files

LeAudioService: Fix handling of lead device disconnection

With this patch we make sure that Active device is cleared properly when
both all set members has been disconnected.
Especially in the use case, when as a first device the Lead device was
disconnected.

Bug: 233362628
Test: atest BluetoothInstrumentationTests

Change-Id: I7187801fe977610c759450dc6fa75d89c70db54a
(cherry picked from commit b20e13fc947f9d5434820e3a10ead62ce1d9973b)
parent b55c3d20
Loading
Loading
Loading
Loading
+26 −13
Original line number Diff line number Diff line
@@ -139,7 +139,7 @@ public class LeAudioService extends ProfileService {
            mIsActive = false;
            mActiveContexts = ACTIVE_CONTEXTS_NONE;
            mCodecStatus = null;
            mLostDevicesWhileStreaming = new ArrayList<>();
            mLostLeadDeviceWhileStreaming = null;
        }

        public Boolean mIsConnected;
@@ -147,7 +147,7 @@ public class LeAudioService extends ProfileService {
        public Integer mActiveContexts;
        public BluetoothLeAudioCodecStatus mCodecStatus;
        /* This can be non empty only for the streaming time */
        List<BluetoothDevice> mLostDevicesWhileStreaming;
        BluetoothDevice mLostLeadDeviceWhileStreaming;
    }

    List<BluetoothLeAudioCodecConfig> mInputLocalCodecCapabilities = new ArrayList<>();
@@ -1066,18 +1066,19 @@ public class LeAudioService extends ProfileService {
    }

    private void clearLostDevicesWhileStreaming(LeAudioGroupDescriptor descriptor) {
        for (BluetoothDevice dev : descriptor.mLostDevicesWhileStreaming) {
            LeAudioStateMachine sm = mStateMachines.get(dev);
            if (sm == null) {
                continue;
        if (DBG) {
            Log.d(TAG, " lost dev: " + descriptor.mLostLeadDeviceWhileStreaming);
        }

        LeAudioStateMachine sm = mStateMachines.get(descriptor.mLostLeadDeviceWhileStreaming);
        if (sm != null) {
            LeAudioStackEvent stackEvent =
                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
            stackEvent.device = dev;
            stackEvent.device = descriptor.mLostLeadDeviceWhileStreaming;
            stackEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED;
            sm.sendMessage(LeAudioStateMachine.STACK_EVENT, stackEvent);
        }
        descriptor.mLostLeadDeviceWhileStreaming = null;
    }

    // Suppressed since this is part of a local process
@@ -1110,14 +1111,17 @@ public class LeAudioService extends ProfileService {
                                        mActiveAudioOutDevice)
                                        || Objects.equals(device, mActiveAudioInDevice))
                                        && (getConnectedPeerDevices(groupId).size() > 1)) {
                                    descriptor.mLostDevicesWhileStreaming.add(device);

                                    if (DBG) Log.d(TAG, "Adding to lost devices : " + device);
                                    descriptor.mLostLeadDeviceWhileStreaming = device;
                                    return;
                                }
                                break;
                            case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
                            case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
                                if (descriptor != null) {
                                    descriptor.mLostDevicesWhileStreaming.remove(device);
                                    if (DBG) Log.d(TAG, "Removing from lost devices : " + device);
                                    descriptor.mLostLeadDeviceWhileStreaming = null;
                                    /* Try to connect other devices from the group */
                                    connectSet(device);
                                }
@@ -1254,6 +1258,7 @@ public class LeAudioService extends ProfileService {
                                    ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
                            notifyGroupStatus = true;
                            /* Clear lost devices */
                            if (DBG) Log.d(TAG, "Clear for group: " + groupId);
                            clearLostDevicesWhileStreaming(descriptor);
                        }
                    } else {
@@ -1538,6 +1543,15 @@ public class LeAudioService extends ProfileService {
                return;
            }

            List<BluetoothDevice> connectedDevices = getConnectedPeerDevices(myGroupId);
            /* Let's check if the last connected device is really connected */
            if (connectedDevices.size() == 1
                    && Objects.equals(connectedDevices.get(0),
                            descriptor.mLostLeadDeviceWhileStreaming)) {
                clearLostDevicesWhileStreaming(descriptor);
                return;
            }

            if (getConnectedPeerDevices(myGroupId).isEmpty()){
                descriptor.mIsConnected = false;
                if (descriptor.mIsActive) {
@@ -2584,9 +2598,8 @@ public class LeAudioService extends ProfileService {
            ProfileService.println(sb, "    mActiveContexts: " + descriptor.mActiveContexts);
            ProfileService.println(sb, "    group lead: " + getConnectedGroupLeadDevice(groupId));
            ProfileService.println(sb, "    first device: " + getFirstDeviceFromGroup(groupId));
            for (BluetoothDevice dev : descriptor.mLostDevicesWhileStreaming) {
                ProfileService.println(sb, "        lost device: " + dev);
            }
            ProfileService.println(sb, "    lost lead device: "
                    + descriptor.mLostLeadDeviceWhileStreaming);
        }
    }
}
+161 −0
Original line number Diff line number Diff line
@@ -26,7 +26,11 @@ import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -42,6 +46,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.BluetoothProfileConnectionInfo;
import android.os.ParcelUuid;

import androidx.test.InstrumentationRegistry;
@@ -68,6 +73,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;

@@ -457,6 +463,23 @@ public class LeAudioServiceTest {
                .isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
    }

    private void injectNoVerifyDeviceConnected(BluetoothDevice device) {
        generateUnexpectedConnectionMessageFromNative(device,
                LeAudioStackEvent.CONNECTION_STATE_CONNECTED,
                LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED);
    }

    private void injectAndVerifyDeviceDisconnected(BluetoothDevice device) {
        generateConnectionMessageFromNative(device,
                LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED,
                LeAudioStackEvent.CONNECTION_STATE_CONNECTED);
    }

    private void injectNoVerifyDeviceDisconnected(BluetoothDevice device) {
        generateUnexpectedConnectionMessageFromNative(device,
                LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED,
                LeAudioStackEvent.CONNECTION_STATE_CONNECTED);
    }
    /**
     * Test that the outgoing connect/disconnect and audio switch is successful.
     */
@@ -1220,4 +1243,142 @@ public class LeAudioServiceTest {
        onGroupCodecConfChangedCallbackCalled = false;
        mService.mLeAudioCallbacks.unregister(leAudioCallbacks);
    }

    private void verifyActiveDeviceStateIntent(int timeoutMs, BluetoothDevice device) {
        Intent intent = TestUtils.waitForIntent(timeoutMs, mDeviceQueueMap.get(device));
        assertThat(intent).isNotNull();
        assertThat(intent.getAction())
                .isEqualTo(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
        assertThat(device).isEqualTo(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
    }

    /**
     * Test native interface group status message handling
     */
    @Test
    public void testLeadGroupDeviceDisconnects() {
        int groupId = 1;
        int direction = 1;
        int snkAudioLocation = 3;
        int srcAudioLocation = 4;
        int availableContexts = 5;
        int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE;
        BluetoothDevice leadDevice;
        BluetoothDevice memberDevice = mLeftDevice;

        doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
        connectTestDevice(mLeftDevice, groupId);
        connectTestDevice(mRightDevice, groupId);

        leadDevice = mService.getConnectedGroupLeadDevice(groupId);
        if (Objects.equals(leadDevice, mLeftDevice)) {
                memberDevice = mRightDevice;
        }

        assertThat(mService.setActiveDevice(leadDevice)).isTrue();

        //Add location support
        LeAudioStackEvent audioConfChangedEvent =
                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
        audioConfChangedEvent.valueInt1 = direction;
        audioConfChangedEvent.valueInt2 = groupId;
        audioConfChangedEvent.valueInt3 = snkAudioLocation;
        audioConfChangedEvent.valueInt4 = srcAudioLocation;
        audioConfChangedEvent.valueInt5 = availableContexts;
        mService.messageFromNative(audioConfChangedEvent);

        //Set group and device as active
        LeAudioStackEvent groupStatusChangedEvent =
                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED);
        groupStatusChangedEvent.valueInt1 = groupId;
        groupStatusChangedEvent.valueInt2 = groupStatus;
        mService.messageFromNative(groupStatusChangedEvent);

        assertThat(mService.getActiveDevices().contains(leadDevice)).isTrue();
        verify(mAudioManager, times(1)).handleBluetoothActiveDeviceChanged(eq(leadDevice), any(),
                        any(BluetoothProfileConnectionInfo.class));

        verifyActiveDeviceStateIntent(TIMEOUT_MS, leadDevice);
        injectNoVerifyDeviceDisconnected(leadDevice);

        // We should not change the audio device
        assertThat(mService.getConnectionState(leadDevice))
                .isEqualTo(BluetoothProfile.STATE_CONNECTED);

        injectAndVerifyDeviceDisconnected(memberDevice);

        // Verify the connection state broadcast, and that we are in Connecting state
        verifyConnectionStateIntent(TIMEOUT_MS, leadDevice, BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.STATE_CONNECTED);

        verify(mAudioManager, times(1)).handleBluetoothActiveDeviceChanged(any(), eq(leadDevice),
                any(BluetoothProfileConnectionInfo.class));

    }

    /**
     * Test native interface group status message handling
     */
    @Test
    public void testLeadGroupDeviceReconnects() {
        int groupId = 1;
        int direction = 1;
        int snkAudioLocation = 3;
        int srcAudioLocation = 4;
        int availableContexts = 5;
        int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE;
        BluetoothDevice leadDevice;
        BluetoothDevice memberDevice = mLeftDevice;

        doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
        connectTestDevice(mLeftDevice, groupId);
        connectTestDevice(mRightDevice, groupId);

        leadDevice = mService.getConnectedGroupLeadDevice(groupId);
        if (Objects.equals(leadDevice, mLeftDevice)) {
                memberDevice = mRightDevice;
        }

        assertThat(mService.setActiveDevice(leadDevice)).isTrue();

        //Add location support
        LeAudioStackEvent audioConfChangedEvent =
                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
        audioConfChangedEvent.valueInt1 = direction;
        audioConfChangedEvent.valueInt2 = groupId;
        audioConfChangedEvent.valueInt3 = snkAudioLocation;
        audioConfChangedEvent.valueInt4 = srcAudioLocation;
        audioConfChangedEvent.valueInt5 = availableContexts;
        mService.messageFromNative(audioConfChangedEvent);

        //Set group and device as active
        LeAudioStackEvent groupStatusChangedEvent =
                new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED);
        groupStatusChangedEvent.valueInt1 = groupId;
        groupStatusChangedEvent.valueInt2 = groupStatus;
        mService.messageFromNative(groupStatusChangedEvent);

        assertThat(mService.getActiveDevices().contains(leadDevice)).isTrue();
        verify(mAudioManager, times(1)).handleBluetoothActiveDeviceChanged(eq(leadDevice), any(),
                        any(BluetoothProfileConnectionInfo.class));

        verifyActiveDeviceStateIntent(TIMEOUT_MS, leadDevice);
        /* We don't want to distribute DISCONNECTION event, instead will try to reconnect
         * (in native)
         */
        injectNoVerifyDeviceDisconnected(leadDevice);
        assertThat(mService.getConnectionState(leadDevice))
                .isEqualTo(BluetoothProfile.STATE_CONNECTED);

        /* Reconnect device, there should be no intent about that, as device was pretending
         * connected
         */
        injectNoVerifyDeviceConnected(leadDevice);

        injectAndVerifyDeviceDisconnected(memberDevice);
        injectAndVerifyDeviceDisconnected(leadDevice);

        verify(mAudioManager, times(1)).handleBluetoothActiveDeviceChanged(eq(null), eq(leadDevice),
                any(BluetoothProfileConnectionInfo.class));
    }
}