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

Commit bf7b5c4d authored by amehfooz's avatar amehfooz
Browse files

[SB][ComposeIcons] Add Bluetooth icon

Bug: 414890231
Test: Make sure bluetooth icon is updated
Screenshot provided in bug.
Flag: com.android.systemui.status_bar_system_status_icons_in_compose

Change-Id: Iccdb89fc0f4927b6b4bb16dedb6edc7ad3e61d2f
parent fc2f2352
Loading
Loading
Loading
Loading
+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),
            )
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -42,9 +42,19 @@ object SystemStatusIconsInCompose {
     * build to ensure that the refactor author catches issues in testing.
     */
    @JvmStatic
    @Deprecated("Use expectInNewMode instead", ReplaceWith("expectInNewMode()"))
    inline fun isUnexpectedlyInLegacyMode() =
        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)

    /**
     * Called to ensure code is only run when the flag is enabled. This protects users from the
     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
     * build to ensure that the refactor author catches issues in testing.
     */
    @JvmStatic
    inline fun expectInNewMode() =
        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)

    /**
     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
     * the flag is enabled to ensure that the refactor author catches issues in testing.
+72 −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 androidx.compose.runtime.getValue
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.bluetooth.domain.interactor.BluetoothConnectionStatusInteractor
import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.statusbar.systemstatusicons.ui.viewmodel.SystemStatusIconViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.map

/**
 * View model for the bluetooth connected system status icon. Emits a bluetooth connected icon when
 * a bluetooth device is connected. Null icon otherwise.
 */
class BluetoothIconViewModel
@AssistedInject
constructor(interactor: BluetoothConnectionStatusInteractor) :
    SystemStatusIconViewModel, ExclusiveActivatable() {
    init {
        SystemStatusIconsInCompose.expectInNewMode()
    }

    private val hydrator = Hydrator("BluetoothIconViewModel.hydrator")

    override val icon: Icon? by
        hydrator.hydratedStateOf(
            traceName = null,
            initialValue = null,
            source = interactor.isBluetoothConnected.map { it.toUiState() },
        )

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    }

    private fun Boolean.toUiState(): Icon? =
        if (this) {
            Icon.Resource(
                res = R.drawable.ic_bluetooth_connected,
                contentDescription =
                    ContentDescription.Resource(R.string.accessibility_bluetooth_connected),
            )
        } else {
            null
        }

    @AssistedFactory
    interface Factory {
        fun create(): BluetoothIconViewModel
    }
}
+5 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import com.android.systemui.common.shared.model.Icon
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.statusbar.systemstatusicons.airplane.ui.viewmodel.AirplaneModeIconViewModel
import com.android.systemui.statusbar.systemstatusicons.bluetooth.ui.viewmodel.BluetoothIconViewModel
import com.android.systemui.statusbar.systemstatusicons.ethernet.ui.viewmodel.EthernetIconViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -37,6 +38,7 @@ class SystemStatusIconsViewModel
@AssistedInject
constructor(
    airplaneModeIconViewModelFactory: AirplaneModeIconViewModel.Factory,
    bluetoothIconViewModelFactory: BluetoothIconViewModel.Factory,
    ethernetIconViewModelFactory: EthernetIconViewModel.Factory,
) : ExclusiveActivatable() {

@@ -46,8 +48,9 @@ constructor(

    private val airplaneModeIcon by lazy { airplaneModeIconViewModelFactory.create() }
    private val ethernetIcon by lazy { ethernetIconViewModelFactory.create() }
    private val bluetoothIcon by lazy { bluetoothIconViewModelFactory.create() }
    private val iconViewModels: List<SystemStatusIconViewModel> by lazy {
        listOf(ethernetIcon, airplaneModeIcon)
        listOf(bluetoothIcon, ethernetIcon, airplaneModeIcon)
    }

    val icons: List<Icon>
@@ -56,6 +59,7 @@ constructor(
    override suspend fun onActivated(): Nothing {
        coroutineScope {
            launch { ethernetIcon.activate() }
            launch { bluetoothIcon.activate() }
            launch { airplaneModeIcon.activate() }
        }
        awaitCancellation()
+30 −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 com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.policy.bluetooth.domain.interactor.bluetoothConnectionStatusInteractor

private val Kosmos.bluetoothIconViewModel: BluetoothIconViewModel by
    Kosmos.Fixture { BluetoothIconViewModel(bluetoothConnectionStatusInteractor) }

val Kosmos.bluetoothIconViewModelFactory: BluetoothIconViewModel.Factory by
    Kosmos.Fixture {
        object : BluetoothIconViewModel.Factory {
            override fun create(): BluetoothIconViewModel = bluetoothIconViewModel
        }
    }
Loading