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

Commit 38a622c1 authored by amehfooz's avatar amehfooz
Browse files

[SB][ComposeIcons] Add connectedDevices StateFlow

This change adds a StateFlow in the BluetoothRepository
that flows on the bgDispatcher with updates to the currently
connected bluetooth devices.
This will be used for RA implementations for bluetooth states
in the status bar.

Bug: 414890231
Test: BluetoothRepositoryImplTest
Flag: com.android.systemui.status_bar_system_status_icons_in_compose
Change-Id: I029be765691da37aad0ce02757f0e1ad0ae9ed4c
parent 1558b8ed
Loading
Loading
Loading
Loading
+101 −1
Original line number Diff line number Diff line
@@ -17,10 +17,14 @@ package com.android.systemui.statusbar.policy.bluetooth
import android.bluetooth.BluetoothProfile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.BluetoothEventManager
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothAdapter
import com.android.systemui.SysuiTestCase
import com.android.systemui.bluetooth.cachedBluetoothDeviceManager
import com.android.systemui.bluetooth.localBluetoothManager
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.policy.bluetooth.data.repository.BluetoothRepositoryImpl
@@ -36,8 +40,11 @@ import kotlinx.coroutines.test.TestScope
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@@ -48,6 +55,12 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
    private val underTest: BluetoothRepositoryImpl =
        kosmos.realBluetoothRepository as BluetoothRepositoryImpl

    private val bluetoothManager = kosmos.localBluetoothManager!!
    @Mock private lateinit var eventManager: BluetoothEventManager
    @Mock private lateinit var cachedDevice: CachedBluetoothDevice
    @Mock private lateinit var cachedDevice2: CachedBluetoothDevice
    @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback>

    private lateinit var scheduler: TestCoroutineScheduler
    private lateinit var dispatcher: TestDispatcher
    private lateinit var testScope: TestScope
@@ -60,7 +73,94 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
        scheduler = TestCoroutineScheduler()
        dispatcher = StandardTestDispatcher(scheduler)
        testScope = TestScope(dispatcher)
        whenever(kosmos.localBluetoothManager?.bluetoothAdapter).thenReturn(bluetoothAdapter)

        whenever(bluetoothManager.eventManager).thenReturn(eventManager)
        whenever(bluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)
    }

    @Test
    fun connectedDevices_initialStateWithNoDevices_isEmpty() =
        kosmos.runTest {
            val connectedDevices by collectLastValue(underTest.connectedDevices)

            bluetoothManager.eventManager.let {
                verify(it).registerCallback(callbackCaptor.capture())
            }

            assertThat(connectedDevices).isEmpty()
        }

    @Test
    fun connectedDevices_whenDeviceConnects_emitsDevice() =
        kosmos.runTest {
            val connectedDevices by collectLastValue(underTest.connectedDevices)
            bluetoothManager.eventManager.let {
                verify(it).registerCallback(callbackCaptor.capture())
            }

            val callback = callbackCaptor.value
            assertThat(connectedDevices).isEmpty()

            // Simulate device connecting
            whenever(cachedDevice.isConnected).thenReturn(true)
            whenever(cachedDevice.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
            whenever(cachedBluetoothDeviceManager.cachedDevicesCopy)
                .thenReturn(listOf(cachedDevice))

            // Trigger a callback
            callback.onConnectionStateChanged(cachedDevice, BluetoothProfile.STATE_CONNECTED)
            assertThat(connectedDevices).isEqualTo(listOf(cachedDevice))
        }

    @Test
    fun connectedDevices_whenDeviceDisconnects_isEmpty() =
        kosmos.runTest {
            val connectedDevices by collectLastValue(underTest.connectedDevices)
            bluetoothManager.eventManager?.let {
                verify(it).registerCallback(callbackCaptor.capture())
            }
            val callback = callbackCaptor.value

            // Start with a connected device
            whenever(cachedDevice.isConnected).thenReturn(true)
            whenever(cachedDevice.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
            whenever(cachedBluetoothDeviceManager.cachedDevicesCopy)
                .thenReturn(listOf(cachedDevice))
            callback.onConnectionStateChanged(cachedDevice, BluetoothProfile.STATE_CONNECTED)
            assertThat(connectedDevices).isNotEmpty()

            // Simulate device disconnecting
            whenever(cachedDevice.isConnected).thenReturn(false)
            whenever(cachedDevice.maxConnectionState)
                .thenReturn(BluetoothProfile.STATE_DISCONNECTED)
            whenever(cachedBluetoothDeviceManager.cachedDevicesCopy).thenReturn(emptyList())

            // Trigger a callback reflecting the disconnection
            callback.onConnectionStateChanged(cachedDevice, BluetoothProfile.STATE_DISCONNECTED)

            assertThat(connectedDevices).isEmpty()
        }

    @Test
    fun connectedDevices_whenMultipleDevicesConnects_emitsAllDevices() =
        kosmos.runTest {
            val connectedDevices by collectLastValue(underTest.connectedDevices)
            bluetoothManager.eventManager.let {
                verify(it).registerCallback(callbackCaptor.capture())
            }

            val callback = callbackCaptor.value
            assertThat(connectedDevices).isEmpty()

            whenever(cachedDevice.isConnected).thenReturn(true)
            whenever(cachedDevice.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
            whenever(cachedDevice2.isConnected).thenReturn(true)
            whenever(cachedDevice2.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
            whenever(cachedBluetoothDeviceManager.cachedDevicesCopy)
                .thenReturn(listOf(cachedDevice, cachedDevice2))
            callback.onConnectionStateChanged(cachedDevice, BluetoothProfile.STATE_CONNECTED)

            assertThat(connectedDevices).isEqualTo(listOf(cachedDevice, cachedDevice2))
        }

    @Test
+92 −0
Original line number Diff line number Diff line
@@ -19,13 +19,22 @@ package com.android.systemui.statusbar.policy.bluetooth.data.repository
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.kairos.awaitClose
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

/**
@@ -36,6 +45,12 @@ import kotlinx.coroutines.withContext
 * now.
 */
interface BluetoothRepository {
    /**
     * A [StateFlow] that emits the current list of [CachedBluetoothDevice] instances that are
     * considered to be connected.
     */
    val connectedDevices: StateFlow<List<CachedBluetoothDevice>>

    /**
     * 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
@@ -57,6 +72,72 @@ constructor(
    @Background private val bgDispatcher: CoroutineDispatcher,
    private val localBluetoothManager: LocalBluetoothManager?,
) : BluetoothRepository {

    override val connectedDevices: StateFlow<List<CachedBluetoothDevice>> =
        conflatedCallbackFlow {
                val callback =
                    object : BluetoothCallback {
                        override fun onBluetoothStateChanged(bluetoothState: Int) {
                            scope.launch {
                                trySendWithFailureLogging(
                                    getCurrentConnectedDevices(),
                                    TAG,
                                    "onBluetoothStateChanged",
                                )
                            }
                        }

                        override fun onConnectionStateChanged(
                            cachedDevice: CachedBluetoothDevice?,
                            state: Int,
                        ) {
                            scope.launch {
                                trySendWithFailureLogging(
                                    getCurrentConnectedDevices(),
                                    TAG,
                                    "onConnectionStateChanged",
                                )
                            }
                        }

                        override fun onProfileConnectionStateChanged(
                            cachedDevice: CachedBluetoothDevice,
                            state: Int,
                            bluetoothProfile: Int,
                        ) {
                            scope.launch {
                                trySendWithFailureLogging(
                                    getCurrentConnectedDevices(),
                                    TAG,
                                    "onProfileConnectionStateChanged",
                                )
                            }
                        }

                        override fun onAclConnectionStateChanged(
                            cachedDevice: CachedBluetoothDevice,
                            state: Int,
                        ) {
                            scope.launch {
                                trySendWithFailureLogging(
                                    getCurrentConnectedDevices(),
                                    TAG,
                                    "onAclConnectionStateChanged",
                                )
                            }
                        }
                    }
                localBluetoothManager?.eventManager?.registerCallback(callback)
                awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(callback) }
            }
            .onStart { scope.launch { getCurrentConnectedDevices() } }
            .flowOn(bgDispatcher)
            .stateIn(
                scope = scope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = emptyList(),
            )

    override fun fetchConnectionStatusInBackground(
        currentDevices: Collection<CachedBluetoothDevice>,
        callback: ConnectionStatusFetchedCallback,
@@ -96,6 +177,17 @@ constructor(
            ConnectionStatusModel(maxConnectionState, connectedDevices)
        }
    }

    private suspend fun getCurrentConnectedDevices(): List<CachedBluetoothDevice> {
        return withContext(bgDispatcher) {
            localBluetoothManager?.cachedDeviceManager?.cachedDevicesCopy?.filter { it.isConnected }
                ?: emptyList()
        }
    }

    companion object {
        private const val TAG = "BluetoothRepositoryImpl"
    }
}

data class ConnectionStatusModel(
+9 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@ package com.android.systemui.statusbar.policy.bluetooth.data.repository

import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
@@ -36,6 +38,13 @@ class FakeBluetoothRepository(localBluetoothManager: LocalBluetoothManager) : Bl
    private val impl =
        BluetoothRepositoryImpl(testScope.backgroundScope, dispatcher, localBluetoothManager)

    private val _connectedDevices = MutableStateFlow(emptyList<CachedBluetoothDevice>())
    override val connectedDevices = _connectedDevices.asStateFlow()

    fun setConnectedDevices(devices: List<CachedBluetoothDevice>) {
        _connectedDevices.value = devices
    }

    override fun fetchConnectionStatusInBackground(
        currentDevices: Collection<CachedBluetoothDevice>,
        callback: ConnectionStatusFetchedCallback,