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

Commit 79714e49 authored by Ahmed Mehfooz's avatar Ahmed Mehfooz Committed by Android (Google) Code Review
Browse files

Merge changes Iccdb89fc,Ib4cd6949,I029be765,I60d1b349 into main

* changes:
  [SB][ComposeIcons] Add Bluetooth icon
  [SB][ComposeIcons] Add interactor for bluetooth connection status
  [SB][ComposeIcons] Add connectedDevices StateFlow
  [SB] Clean up BluetoothRepository
parents 8d4ac3cd bf7b5c4d
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -47,8 +47,8 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.bluetooth.BluetoothLogger;
import com.android.systemui.dump.DumpManager;
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.statusbar.policy.bluetooth.data.repository.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.data.repository.FakeBluetoothRepository;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;

+234 −114
Original line number Diff line number Diff line
@@ -17,12 +17,21 @@ 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.settingslib.bluetooth.LocalBluetoothManager
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
import com.android.systemui.statusbar.policy.bluetooth.data.repository.ConnectionStatusModel
import com.android.systemui.statusbar.policy.bluetooth.data.repository.realBluetoothRepository
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -31,37 +40,134 @@ 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
@RunWith(AndroidJUnit4::class)
class BluetoothRepositoryImplTest : SysuiTestCase() {

    private lateinit var underTest: BluetoothRepositoryImpl
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    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

    @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)
        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 fetchConnectionStatusInBackground_currentDevicesEmpty_maxStateIsManagerState() {
    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
    fun fetchConnectionStatusInBackground_currentDevicesEmpty_maxStateIsManagerState() =
        kosmos.runTest {
            whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)

            val status = fetchConnectionStatus(currentDevices = emptyList())
@@ -70,41 +176,46 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
        }

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

        val status = fetchConnectionStatus(currentDevices = emptyList())
            val status =
                fetchConnectionStatus(repository = repository, currentDevices = emptyList())

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

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

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

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

    @Test
    fun fetchConnectionStatusInBackground_oneCurrentDevice_maxStateIsDeviceState() {
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
    fun fetchConnectionStatusInBackground_oneCurrentDevice_maxStateIsDeviceState() =
        kosmos.runTest {
            whenever(bluetoothAdapter.connectionState)
                .thenReturn(BluetoothProfile.STATE_DISCONNECTED)
            val device =
                mock<CachedBluetoothDevice>().also {
                    whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
@@ -116,27 +227,30 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
        }

    @Test
    fun fetchConnectionStatusInBackground_multipleDevices_maxStateIsHighestState() {
        whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
    fun fetchConnectionStatusInBackground_multipleDevices_maxStateIsHighestState() =
        kosmos.runTest {
            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 =
            val cachedDevice2 =
                mock<CachedBluetoothDevice>().also {
                    whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
                    whenever(it.isConnected).thenReturn(true)
                }

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

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

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

            // WHEN the devices say their state is CONNECTED but [isConnected] is false
@@ -145,27 +259,29 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
                    whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
                    whenever(it.isConnected).thenReturn(false)
                }
        val device2 =
            val cachedDevice2 =
                mock<CachedBluetoothDevice>().also {
                    whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
                    whenever(it.isConnected).thenReturn(false)
                }

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

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

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

            assertThat(status.connectedDevices).isEmpty()
        }

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

@@ -175,7 +291,8 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
        }

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

@@ -185,7 +302,8 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
        }

    @Test
    fun fetchConnectionStatusInBackground_multipleDevices_connectedDevicesHasOnlyConnected() {
    fun fetchConnectionStatusInBackground_multipleDevices_connectedDevicesHasOnlyConnected() =
        kosmos.runTest {
            val device1Connected =
                mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
            val device2Disconnected =
@@ -198,14 +316,16 @@ class BluetoothRepositoryImplTest : SysuiTestCase() {
                    currentDevices = listOf(device1Connected, device2Disconnected, device3Connected)
                )

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

    private fun fetchConnectionStatus(
        currentDevices: Collection<CachedBluetoothDevice>
        repository: BluetoothRepositoryImpl = underTest,
        currentDevices: Collection<CachedBluetoothDevice>,
    ): ConnectionStatusModel {
        var receivedStatus: ConnectionStatusModel? = null
        underTest.fetchConnectionStatusInBackground(currentDevices) { status ->
        repository.fetchConnectionStatusInBackground(currentDevices) { status ->
            receivedStatus = status
        }
        scheduler.runCurrent()
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.domain.interactor

import android.bluetooth.BluetoothProfile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.systemui.SysuiTestCase
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.bluetoothRepository
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
class BluetoothConnectionStatusInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest = kosmos.bluetoothConnectionStatusInteractor

    @Test
    fun isBluetoothConnected_initialStateWithNoDevices_isFalse() =
        kosmos.runTest {
            val isConnected by collectLastValue(underTest.isBluetoothConnected)

            assertThat(isConnected).isFalse()
        }

    @Test
    fun isBluetoothConnected_whenDeviceConnects_isTrue() =
        kosmos.runTest {
            val isConnected by collectLastValue(underTest.isBluetoothConnected)

            // Simulate device connecting
            bluetoothRepository.setConnectedDevices(listOf(cachedDevice))

            assertThat(isConnected).isTrue()
        }

    @Test
    fun isBluetoothConnected_whenDeviceDisconnects_isFalse() =
        kosmos.runTest {
            val isConnected by collectLastValue(underTest.isBluetoothConnected)

            bluetoothRepository.setConnectedDevices(listOf(cachedDevice))
            assertThat(isConnected).isTrue()

            // Simulate device disconnecting
            bluetoothRepository.setConnectedDevices(emptyList())
            assertThat(isConnected).isFalse()
        }

    companion object {
        val cachedDevice =
            mock<CachedBluetoothDevice>().apply {
                whenever(this.isConnected).thenReturn(true)
                whenever(this.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
            }
    }
}
+97 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemstatusicons.bluetooth.ui.viewmodel

import android.bluetooth.BluetoothProfile
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.bluetooth.data.repository.bluetoothRepository
import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(SystemStatusIconsInCompose.FLAG_NAME)
class BluetoothIconViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    @Mock private lateinit var cachedDevice: CachedBluetoothDevice
    private lateinit var underTest: BluetoothIconViewModel

    @Before
    fun setUp() {
        MockitoAnnotations.openMocks(this)
        whenever(cachedDevice.isConnected).thenReturn(true)
        whenever(cachedDevice.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)

        underTest = kosmos.bluetoothIconViewModelFactory.create()
        underTest.activateIn(kosmos.testScope)
    }

    @Test
    fun icon_bluetoothNotConnected_outputsNull() =
        kosmos.runTest { assertThat(underTest.icon).isNull() }

    @Test
    fun icon_bluetoothConnected_outputsIcon() =
        kosmos.runTest {
            // Simulate device connecting
            bluetoothRepository.setConnectedDevices(listOf(cachedDevice))

            assertThat(underTest.icon).isEqualTo(expectedBluetoothIcon)
        }

    @Test
    fun icon_updatesWhenBluetoothConnectionChanges() =
        kosmos.runTest {
            assertThat(underTest.icon).isNull()

            // Simulate device connecting
            bluetoothRepository.setConnectedDevices(listOf(cachedDevice))
            assertThat(underTest.icon).isEqualTo(expectedBluetoothIcon)

            // Simulate device disconnecting
            bluetoothRepository.setConnectedDevices(emptyList())
            assertThat(underTest.icon).isNull()
        }

    companion object {
        private val expectedBluetoothIcon =
            Icon.Resource(
                res = R.drawable.ic_bluetooth_connected,
                contentDescription =
                    ContentDescription.Resource(R.string.accessibility_bluetooth_connected),
            )
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -42,8 +42,8 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.ConnectionStatusModel;
import com.android.systemui.statusbar.policy.bluetooth.data.repository.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.data.repository.ConnectionStatusModel;

import java.io.PrintWriter;
import java.util.ArrayList;
Loading