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

Commit 4c082a2d authored by Caitlin Shkuratov's avatar Caitlin Shkuratov Committed by Android (Google) Code Review
Browse files

Merge "[Bluetooth] Define bluetooth repo to make fetches in the background." into udc-dev

parents 4918bf7b d280be74
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -38,7 +38,11 @@ import com.android.systemui.bluetooth.BluetoothLogger;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.ConnectionStatusModel;

import java.io.PrintWriter;
import java.util.ArrayList;
@@ -50,14 +54,20 @@ import java.util.concurrent.Executor;
import javax.inject.Inject;

/**
 * Controller for information about bluetooth connections.
 *
 * Note: Right now, this class and {@link BluetoothRepository} co-exist. Any new code should go in
 * {@link BluetoothRepository}, but external clients should query this file for now.
 */
@SysUISingleton
public class BluetoothControllerImpl implements BluetoothController, BluetoothCallback,
        CachedBluetoothDevice.Callback, LocalBluetoothProfileManager.ServiceListener {
    private static final String TAG = "BluetoothController";

    private final FeatureFlags mFeatureFlags;
    private final DumpManager mDumpManager;
    private final BluetoothLogger mLogger;
    private final BluetoothRepository mBluetoothRepository;
    private final LocalBluetoothManager mLocalBluetoothManager;
    private final UserManager mUserManager;
    private final int mCurrentUser;
@@ -79,14 +89,18 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
    @Inject
    public BluetoothControllerImpl(
            Context context,
            FeatureFlags featureFlags,
            UserTracker userTracker,
            DumpManager dumpManager,
            BluetoothLogger logger,
            BluetoothRepository bluetoothRepository,
            @Main Looper mainLooper,
            @Nullable LocalBluetoothManager localBluetoothManager,
            @Nullable BluetoothAdapter bluetoothAdapter) {
        mFeatureFlags = featureFlags;
        mDumpManager = dumpManager;
        mLogger = logger;
        mBluetoothRepository = bluetoothRepository;
        mLocalBluetoothManager = localBluetoothManager;
        mHandler = new H(mainLooper);
        if (mLocalBluetoothManager != null) {
@@ -229,6 +243,16 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
    }

    private void updateConnected() {
        if (mFeatureFlags.isEnabled(Flags.NEW_BLUETOOTH_REPOSITORY)) {
            mBluetoothRepository.fetchConnectionStatusInBackground(
                    getDevices(), this::onConnectionStatusFetched);
        } else {
            updateConnectedOld();
        }
    }

    /** Used only if {@link Flags.NEW_BLUETOOTH_REPOSITORY} is *not* enabled. */
    private void updateConnectedOld() {
        // Make sure our connection state is up to date.
        int state = mLocalBluetoothManager.getBluetoothAdapter().getConnectionState();
        List<CachedBluetoothDevice> newList = new ArrayList<>();
@@ -249,6 +273,12 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
            // connected.
            state = BluetoothAdapter.STATE_DISCONNECTED;
        }
        onConnectionStatusFetched(new ConnectionStatusModel(state, newList));
    }

    private void onConnectionStatusFetched(ConnectionStatusModel status) {
        List<CachedBluetoothDevice> newList = status.getConnectedDevices();
        int state = status.getMaxConnectionState();
        synchronized (mConnectedDevices) {
            mConnectedDevices.clear();
            mConnectedDevices.addAll(newList);
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui.statusbar.policy.bluetooth

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 * Repository for information about bluetooth connections.
 *
 * Note: Right now, this class and [BluetoothController] co-exist. Any new code should go in this
 * implementation, but external clients should query [BluetoothController] instead of this class for
 * now.
 */
interface BluetoothRepository {
    /**
     * Fetches the connection statuses for the given [currentDevices] and invokes [callback] once
     * those statuses have been fetched. The fetching occurs on a background thread because IPCs may
     * be required to fetch the statuses (see b/271058380).
     */
    fun fetchConnectionStatusInBackground(
        currentDevices: Collection<CachedBluetoothDevice>,
        callback: ConnectionStatusFetchedCallback,
    )
}

/** Implementation of [BluetoothRepository]. */
@SysUISingleton
class BluetoothRepositoryImpl
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    @Background private val bgDispatcher: CoroutineDispatcher,
    private val localBluetoothManager: LocalBluetoothManager?,
) : BluetoothRepository {
    override fun fetchConnectionStatusInBackground(
        currentDevices: Collection<CachedBluetoothDevice>,
        callback: ConnectionStatusFetchedCallback,
    ) {
        scope.launch {
            val result = fetchConnectionStatus(currentDevices)
            callback.onConnectionStatusFetched(result)
        }
    }

    private suspend fun fetchConnectionStatus(
        currentDevices: Collection<CachedBluetoothDevice>,
    ): ConnectionStatusModel {
        return withContext(bgDispatcher) {
            val minimumMaxConnectionState =
                localBluetoothManager?.bluetoothAdapter?.connectionState
                    ?: BluetoothProfile.STATE_DISCONNECTED
            var maxConnectionState =
                if (currentDevices.isEmpty()) {
                    minimumMaxConnectionState
                } else {
                    currentDevices
                        .maxOf { it.maxConnectionState }
                        .coerceAtLeast(minimumMaxConnectionState)
                }

            val connectedDevices = currentDevices.filter { it.isConnected }

            if (
                connectedDevices.isEmpty() && maxConnectionState == BluetoothAdapter.STATE_CONNECTED
            ) {
                // If somehow we think we are connected, but have no connected devices, we aren't
                // connected.
                maxConnectionState = BluetoothAdapter.STATE_DISCONNECTED
            }

            ConnectionStatusModel(maxConnectionState, connectedDevices)
        }
    }
}

data class ConnectionStatusModel(
    /** The maximum connection state out of all current devices. */
    val maxConnectionState: Int,
    /** A list of devices that are currently connected. */
    val connectedDevices: List<CachedBluetoothDevice>,
)

/** Callback notified when the new status has been fetched. */
fun interface ConnectionStatusFetchedCallback {
    fun onConnectionStatusFetched(status: ConnectionStatusModel)
}
+9 −4
Original line number Diff line number Diff line
@@ -62,15 +62,16 @@ import com.android.systemui.statusbar.policy.WalletController;
import com.android.systemui.statusbar.policy.WalletControllerImpl;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.policy.ZenModeControllerImpl;

import java.util.concurrent.Executor;

import javax.inject.Named;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepositoryImpl;

import dagger.Binds;
import dagger.Module;
import dagger.Provides;

import java.util.concurrent.Executor;

import javax.inject.Named;

/** Dagger Module for code in the statusbar.policy package. */
@Module
@@ -82,6 +83,10 @@ public interface StatusBarPolicyModule {
    @Binds
    BluetoothController provideBluetoothController(BluetoothControllerImpl controllerImpl);

    /** */
    @Binds
    BluetoothRepository provideBluetoothRepository(BluetoothRepositoryImpl impl);

    /** */
    @Binds
    CastController provideCastController(CastControllerImpl controllerImpl);
+117 −5
Original line number Diff line number Diff line
@@ -14,6 +14,8 @@

package com.android.systemui.statusbar.policy;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -44,7 +46,11 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.bluetooth.BluetoothLogger;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.FakeBluetoothRepository;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

@@ -69,6 +75,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
    private DumpManager mMockDumpManager;
    private BluetoothControllerImpl mBluetoothControllerImpl;
    private BluetoothAdapter mMockAdapter;
    private final FakeFeatureFlags mFakeFeatureFlags = new FakeFeatureFlags();

    private List<CachedBluetoothDevice> mDevices;

@@ -89,17 +96,26 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
                .thenReturn(mock(LocalBluetoothProfileManager.class));
        mMockDumpManager = mock(DumpManager.class);

        mBluetoothControllerImpl = new BluetoothControllerImpl(mContext,
        BluetoothRepository bluetoothRepository =
                new FakeBluetoothRepository(mMockBluetoothManager);
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);

        mBluetoothControllerImpl = new BluetoothControllerImpl(
                mContext,
                mFakeFeatureFlags,
                mUserTracker,
                mMockDumpManager,
                mock(BluetoothLogger.class),
                bluetoothRepository,
                mTestableLooper.getLooper(),
                mMockBluetoothManager,
                mMockAdapter);
    }

    @Test
    public void testNoConnectionWithDevices() {
    public void testNoConnectionWithDevices_repoFlagOff() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);

        CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
        when(device.isConnected()).thenReturn(true);
        when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);
@@ -113,7 +129,39 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
    }

    @Test
    public void testOnServiceConnected_updatesConnectionState() {
    public void testNoConnectionWithDevices_repoFlagOn() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);

        CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
        when(device.isConnected()).thenReturn(true);
        when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);

        mDevices.add(device);
        when(mMockLocalAdapter.getConnectionState())
                .thenReturn(BluetoothAdapter.STATE_DISCONNECTED);

        mBluetoothControllerImpl.onConnectionStateChanged(null,
                BluetoothAdapter.STATE_DISCONNECTED);

        assertTrue(mBluetoothControllerImpl.isBluetoothConnected());
    }

    @Test
    public void testOnServiceConnected_updatesConnectionState_repoFlagOff() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);

        when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING);

        mBluetoothControllerImpl.onServiceConnected();

        assertTrue(mBluetoothControllerImpl.isBluetoothConnecting());
        assertFalse(mBluetoothControllerImpl.isBluetoothConnected());
    }

    @Test
    public void testOnServiceConnected_updatesConnectionState_repoFlagOn() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);

        when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING);

        mBluetoothControllerImpl.onServiceConnected();
@@ -122,6 +170,46 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
        assertFalse(mBluetoothControllerImpl.isBluetoothConnected());
    }

    @Test
    public void getConnectedDevices_onlyReturnsConnected_repoFlagOff() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);

        CachedBluetoothDevice device1Disconnected = mock(CachedBluetoothDevice.class);
        when(device1Disconnected.isConnected()).thenReturn(false);
        mDevices.add(device1Disconnected);

        CachedBluetoothDevice device2Connected = mock(CachedBluetoothDevice.class);
        when(device2Connected.isConnected()).thenReturn(true);
        mDevices.add(device2Connected);

        mBluetoothControllerImpl.onDeviceAdded(device1Disconnected);
        mBluetoothControllerImpl.onDeviceAdded(device2Connected);

        assertThat(mBluetoothControllerImpl.getConnectedDevices()).hasSize(1);
        assertThat(mBluetoothControllerImpl.getConnectedDevices().get(0))
                .isEqualTo(device2Connected);
    }

    @Test
    public void getConnectedDevices_onlyReturnsConnected_repoFlagOn() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);

        CachedBluetoothDevice device1Disconnected = mock(CachedBluetoothDevice.class);
        when(device1Disconnected.isConnected()).thenReturn(false);
        mDevices.add(device1Disconnected);

        CachedBluetoothDevice device2Connected = mock(CachedBluetoothDevice.class);
        when(device2Connected.isConnected()).thenReturn(true);
        mDevices.add(device2Connected);

        mBluetoothControllerImpl.onDeviceAdded(device1Disconnected);
        mBluetoothControllerImpl.onDeviceAdded(device2Connected);

        assertThat(mBluetoothControllerImpl.getConnectedDevices()).hasSize(1);
        assertThat(mBluetoothControllerImpl.getConnectedDevices().get(0))
                .isEqualTo(device2Connected);
    }

    @Test
    public void testOnBluetoothStateChange_updatesBluetoothState() {
        mBluetoothControllerImpl.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
@@ -147,8 +235,9 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
    }

    @Test
    public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection()
            throws Exception {
    public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection_repoFlagOff() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);

        BluetoothController.Callback callback = mock(BluetoothController.Callback.class);
        mBluetoothControllerImpl.addCallback(callback);

@@ -167,6 +256,29 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
        verify(callback, atLeastOnce()).onBluetoothStateChange(anyBoolean());
    }

    @Test
    public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection_repoFlagOn() {
        mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);

        BluetoothController.Callback callback = mock(BluetoothController.Callback.class);
        mBluetoothControllerImpl.addCallback(callback);

        assertFalse(mBluetoothControllerImpl.isBluetoothConnected());
        CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
        mDevices.add(device);
        when(device.isConnected()).thenReturn(true);
        when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);
        reset(callback);
        mBluetoothControllerImpl.onAclConnectionStateChanged(device,
                BluetoothProfile.STATE_CONNECTED);

        mTestableLooper.processAllMessages();

        assertTrue(mBluetoothControllerImpl.isBluetoothConnected());
        verify(callback, atLeastOnce()).onBluetoothStateChange(anyBoolean());
    }


    @Test
    public void testOnActiveDeviceChanged_updatesAudioActive() {
        assertFalse(mBluetoothControllerImpl.isBluetoothAudioActive());
+213 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui.statusbar.policy.bluetooth

import android.bluetooth.BluetoothProfile
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothAdapter
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class BluetoothRepositoryImplTest : SysuiTestCase() {

    private lateinit var underTest: BluetoothRepositoryImpl

    private lateinit var scheduler: TestCoroutineScheduler
    private lateinit var dispatcher: TestDispatcher
    private lateinit var testScope: TestScope

    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
    @Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        whenever(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)

        scheduler = TestCoroutineScheduler()
        dispatcher = StandardTestDispatcher(scheduler)
        testScope = TestScope(dispatcher)

        underTest =
            BluetoothRepositoryImpl(testScope.backgroundScope, dispatcher, localBluetoothManager)
    }

    @Test
    fun fetchConnectionStatusInBackground_currentDevicesEmpty_maxStateIsManagerState() {
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)

        val status = fetchConnectionStatus(currentDevices = emptyList())

        assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
    }

    @Test
    fun fetchConnectionStatusInBackground_currentDevicesEmpty_nullManager_maxStateIsDisconnected() {
        // This CONNECTING state should be unused because localBluetoothManager is null
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
        underTest =
            BluetoothRepositoryImpl(
                testScope.backgroundScope,
                dispatcher,
                localBluetoothManager = null,
            )

        val status = fetchConnectionStatus(currentDevices = emptyList())

        assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED)
    }

    @Test
    fun fetchConnectionStatusInBackground_managerStateLargerThanDeviceStates_maxStateIsManager() {
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
        val device1 =
            mock<CachedBluetoothDevice>().also {
                whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
            }
        val device2 =
            mock<CachedBluetoothDevice>().also {
                whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
            }

        val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))

        assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
    }

    @Test
    fun fetchConnectionStatusInBackground_oneCurrentDevice_maxStateIsDeviceState() {
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
        val device =
            mock<CachedBluetoothDevice>().also {
                whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
            }

        val status = fetchConnectionStatus(currentDevices = listOf(device))

        assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
    }

    @Test
    fun fetchConnectionStatusInBackground_multipleDevices_maxStateIsHighestState() {
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)

        val device1 =
            mock<CachedBluetoothDevice>().also {
                whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
                whenever(it.isConnected).thenReturn(false)
            }
        val device2 =
            mock<CachedBluetoothDevice>().also {
                whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
                whenever(it.isConnected).thenReturn(true)
            }

        val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))

        assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTED)
    }

    @Test
    fun fetchConnectionStatusInBackground_devicesNotConnected_maxStateIsDisconnected() {
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)

        // WHEN the devices say their state is CONNECTED but [isConnected] is false
        val device1 =
            mock<CachedBluetoothDevice>().also {
                whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
                whenever(it.isConnected).thenReturn(false)
            }
        val device2 =
            mock<CachedBluetoothDevice>().also {
                whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
                whenever(it.isConnected).thenReturn(false)
            }

        val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))

        // THEN the max state is DISCONNECTED
        assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED)
    }

    @Test
    fun fetchConnectionStatusInBackground_currentDevicesEmpty_connectedDevicesEmpty() {
        val status = fetchConnectionStatus(currentDevices = emptyList())

        assertThat(status.connectedDevices).isEmpty()
    }

    @Test
    fun fetchConnectionStatusInBackground_oneCurrentDeviceDisconnected_connectedDevicesEmpty() {
        val device =
            mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) }

        val status = fetchConnectionStatus(currentDevices = listOf(device))

        assertThat(status.connectedDevices).isEmpty()
    }

    @Test
    fun fetchConnectionStatusInBackground_oneCurrentDeviceConnected_connectedDevicesHasDevice() {
        val device =
            mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }

        val status = fetchConnectionStatus(currentDevices = listOf(device))

        assertThat(status.connectedDevices).isEqualTo(listOf(device))
    }

    @Test
    fun fetchConnectionStatusInBackground_multipleDevices_connectedDevicesHasOnlyConnected() {
        val device1Connected =
            mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
        val device2Disconnected =
            mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) }
        val device3Connected =
            mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }

        val status =
            fetchConnectionStatus(
                currentDevices = listOf(device1Connected, device2Disconnected, device3Connected)
            )

        assertThat(status.connectedDevices).isEqualTo(listOf(device1Connected, device3Connected))
    }

    private fun fetchConnectionStatus(
        currentDevices: Collection<CachedBluetoothDevice>
    ): ConnectionStatusModel {
        var receivedStatus: ConnectionStatusModel? = null
        underTest.fetchConnectionStatusInBackground(currentDevices) { status ->
            receivedStatus = status
        }
        scheduler.runCurrent()
        return receivedStatus!!
    }
}
Loading