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

Commit e4c1c877 authored by Łukasz Rymanowski's avatar Łukasz Rymanowski
Browse files

LeAudioService: Fix handling of lead device disconnection

With this patch we make sure, 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
Tag: #feature

Change-Id: I7187801fe977610c759450dc6fa75d89c70db54a
Merged-In: I7187801fe977610c759450dc6fa75d89c70db54a
parent 2c0ce6f8
Loading
Loading
Loading
Loading
+26 −13
Original line number Diff line number Diff line
@@ -138,7 +138,7 @@ public class LeAudioService extends ProfileService {
            mIsActive = false;
            mActiveContexts = ACTIVE_CONTEXTS_NONE;
            mCodecStatus = null;
            mLostDevicesWhileStreaming = new ArrayList<>();
            mLostLeadDeviceWhileStreaming = null;
        }

        public Boolean mIsConnected;
@@ -146,7 +146,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<>();
@@ -1054,18 +1054,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
@@ -1098,14 +1099,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);
                                }
@@ -1242,6 +1246,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 {
@@ -1528,6 +1533,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) {
@@ -2574,9 +2588,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);
        }
    }
}
+168 −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;
@@ -457,6 +462,29 @@ public class LeAudioServiceTest {
                .isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
    }

    private void injectAndVerifyDeviceConnected(BluetoothDevice device) {
        generateConnectionMessageFromNative(device,
                LeAudioStackEvent.CONNECTION_STATE_CONNECTED,
                LeAudioStackEvent.CONNECTION_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 +1248,144 @@ 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 nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED;
        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 (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(BluetoothProfile.STATE_CONNECTED)
                .isEqualTo(mService.getConnectionState(leadDevice));

        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 nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED;
        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 (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(BluetoothProfile.STATE_CONNECTED)
                .isEqualTo(mService.getConnectionState(leadDevice));

        /* 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));
    }
}