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

Commit 4f07b4ea authored by Ahmed Mehfooz's avatar Ahmed Mehfooz
Browse files

[SB][ComposeIcons] Add Connected Display Icon

Test: ConnectedDisplayIconViewModelTest
Bug: 418422294
Flag: com.android.systemui.status_bar_system_status_icons_in_compose
Change-Id: I9b59244c501be099162e6f0b90393cd68d707b81
parent 0401a56f
Loading
Loading
Loading
Loading
+109 −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.connecteddisplay.ui.viewmodel

import android.content.testableContext
import android.platform.test.annotations.EnableFlags
import android.view.Display.FLAG_SECURE
import android.view.Display.TYPE_EXTERNAL
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
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.display.data.repository.display
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
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.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

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

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val keyguardRepository = kosmos.fakeKeyguardRepository

    private val underTest =
        kosmos.connectedDisplayIconViewModelFactory.create(kosmos.testableContext).apply {
            activateIn(kosmos.testScope)
        }

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

    @Test
    fun icon_displayConnected_outputsIcon() =
        kosmos.runTest {
            keyguardRepository.setKeyguardShowing(true)
            displayRepository.setDefaultDisplayOff(false)
            displayRepository.addDisplay(display(type = TYPE_EXTERNAL, id = 1))

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

    @Test
    fun icon_displayConnectedSecure_outputsIcon() =
        kosmos.runTest {
            keyguardRepository.setKeyguardShowing(false)
            displayRepository.setDefaultDisplayOff(false)
            displayRepository.addDisplay(display(type = TYPE_EXTERNAL, flags = FLAG_SECURE, id = 1))

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

    @Test
    fun icon_updatesWhenDisplayConnectionChanges() =
        kosmos.runTest {
            displayRepository.setDefaultDisplayOff(false)
            assertThat(underTest.icon).isNull()

            keyguardRepository.setKeyguardShowing(true)
            displayRepository.addDisplay(display(type = TYPE_EXTERNAL, id = 1))

            assertThat(underTest.icon).isEqualTo(expectedConnectedDisplayIcon)

            displayRepository.removeDisplay(1)
            assertThat(underTest.icon).isNull()

            keyguardRepository.setKeyguardShowing(false)
            displayRepository.addDisplay(display(type = TYPE_EXTERNAL, flags = FLAG_SECURE, id = 2))
            assertThat(underTest.icon).isEqualTo(expectedConnectedDisplayIcon)

            displayRepository.removeDisplay(2)
            assertThat(underTest.icon).isNull()
        }

    companion object {
        private val expectedConnectedDisplayIcon =
            Icon.Resource(
                res = R.drawable.stat_sys_connected_display,
                contentDescription =
                    ContentDescription.Resource(R.string.connected_display_icon_desc),
            )
    }
}
+38 −5
Original line number Diff line number Diff line
@@ -20,12 +20,16 @@ import android.bluetooth.BluetoothProfile
import android.content.testableContext
import android.media.AudioManager
import android.platform.test.annotations.EnableFlags
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.display
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
@@ -65,6 +69,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {

    private lateinit var slotAirplane: String
    private lateinit var slotBluetooth: String
    private lateinit var slotConnectedDisplay: String
    private lateinit var slotEthernet: String
    private lateinit var slotMute: String
    private lateinit var slotVibrate: String
@@ -75,6 +80,8 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
    fun setUp() {
        slotAirplane = context.getString(com.android.internal.R.string.status_bar_airplane)
        slotBluetooth = context.getString(com.android.internal.R.string.status_bar_bluetooth)
        slotConnectedDisplay =
            context.getString(com.android.internal.R.string.status_bar_connected_display)
        slotEthernet = context.getString(com.android.internal.R.string.status_bar_ethernet)
        slotMute = context.getString(com.android.internal.R.string.status_bar_mute)
        slotVibrate = context.getString(com.android.internal.R.string.status_bar_volume)
@@ -173,23 +180,36 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {

            showZenMode()
            showBluetooth()
            showConnectedDisplay()
            showAirplaneMode()
            showEthernet()
            showVibrate()
            showWifi()

            assertThat(underTest.activeSlotNames)
                .containsExactly(slotAirplane, slotBluetooth, slotEthernet, slotVibrate, slotZen)
                .containsExactly(
                    slotAirplane,
                    slotBluetooth,
                    slotConnectedDisplay,
                    slotEthernet,
                    slotVibrate,
                    slotZen,
                )
                .inOrder()

            // The [mute,vibrate] and [ethernet, wifi] icons can not be shown at the same time so we
            // have to test it
            // separately.
            // have to test it separately.
            showMute() // This will make vibrate inactive
            showWifi() // This will make ethernet inactive

            assertThat(underTest.activeSlotNames)
                .containsExactly(slotAirplane, slotBluetooth, slotMute, slotWifi, slotZen)
                .containsExactly(
                    slotAirplane,
                    slotBluetooth,
                    slotConnectedDisplay,
                    slotMute,
                    slotWifi,
                    slotZen,
                )
                .inOrder()
        }

@@ -215,6 +235,19 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
        )
    }

    private suspend fun Kosmos.showConnectedDisplay(isSecure: Boolean = false) {
        fakeKeyguardRepository.setKeyguardShowing(!isSecure)
        displayRepository.setDefaultDisplayOff(false)
        val flags = if (isSecure) Display.FLAG_SECURE else 0
        displayRepository.addDisplay(
            display(
                type = Display.TYPE_EXTERNAL,
                flags = flags,
                id = (displayRepository.displays.value.maxOfOrNull { it.displayId } ?: 0) + 1,
            )
        )
    }

    private fun Kosmos.showEthernet() {
        connectivityRepository.fake.setEthernetConnected(default = true, validated = true)
    }
+76 −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.connecteddisplay.ui.viewmodel

import android.content.Context
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.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.res.R
import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.statusbar.systemstatusicons.ui.viewmodel.SystemStatusIconViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.map

/**
 * View model for the connected display system status icon. Emits a connected display icon when an
 * external display is connected. Null icon otherwise.
 */
class ConnectedDisplayIconViewModel
@AssistedInject
constructor(@Assisted private val context: Context, interactor: ConnectedDisplayInteractor) :
    SystemStatusIconViewModel, ExclusiveActivatable() {
    init {
        SystemStatusIconsInCompose.expectInNewMode()
    }

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

    override val slotName =
        context.getString(com.android.internal.R.string.status_bar_connected_display)

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

    override suspend fun onActivated(): Nothing = hydrator.activate()

    private fun ConnectedDisplayInteractor.State.toUiState(): Icon? =
        when (this) {
            ConnectedDisplayInteractor.State.CONNECTED,
            ConnectedDisplayInteractor.State.CONNECTED_SECURE ->
                Icon.Resource(
                    res = R.drawable.stat_sys_connected_display,
                    contentDescription =
                        ContentDescription.Resource(R.string.connected_display_icon_desc),
                )
            ConnectedDisplayInteractor.State.DISCONNECTED -> null
        }

    @AssistedFactory
    interface Factory {
        fun create(context: Context): ConnectedDisplayIconViewModel
    }
}
+9 −1
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.systemui.lifecycle.Hydrator
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.connecteddisplay.ui.viewmodel.ConnectedDisplayIconViewModel
import com.android.systemui.statusbar.systemstatusicons.domain.interactor.OrderedIconSlotNamesInteractor
import com.android.systemui.statusbar.systemstatusicons.ethernet.ui.viewmodel.EthernetIconViewModel
import com.android.systemui.statusbar.systemstatusicons.ringer.ui.viewmodel.MuteIconViewModel
@@ -51,6 +52,7 @@ constructor(
    orderedIconSlotNamesInteractor: OrderedIconSlotNamesInteractor,
    airplaneModeIconViewModelFactory: AirplaneModeIconViewModel.Factory,
    bluetoothIconViewModelFactory: BluetoothIconViewModel.Factory,
    connectedDisplayIconViewModelFactory: ConnectedDisplayIconViewModel.Factory,
    ethernetIconViewModelFactory: EthernetIconViewModel.Factory,
    muteIconViewModelFactory: MuteIconViewModel.Factory,
    vibrateIconViewModelFactory: VibrateIconViewModel.Factory,
@@ -66,6 +68,9 @@ constructor(

    private val airplaneModeIcon by lazy { airplaneModeIconViewModelFactory.create(context) }
    private val bluetoothIcon by lazy { bluetoothIconViewModelFactory.create(context) }
    private val connectedDisplayIcon by lazy {
        connectedDisplayIconViewModelFactory.create(context)
    }
    private val ethernetIcon by lazy { ethernetIconViewModelFactory.create(context) }
    private val muteIcon by lazy { muteIconViewModelFactory.create(context) }
    private val vibrateIcon by lazy { vibrateIconViewModelFactory.create(context) }
@@ -76,6 +81,7 @@ constructor(
        listOf(
            airplaneModeIcon,
            bluetoothIcon,
            connectedDisplayIcon,
            ethernetIcon,
            muteIcon,
            vibrateIcon,
@@ -104,9 +110,11 @@ constructor(

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

            launch { airplaneModeIcon.activate() }
            launch { bluetoothIcon.activate() }
            launch { hydrator.activate() }
            launch { connectedDisplayIcon.activate() }
            launch { ethernetIcon.activate() }
            launch { muteIcon.activate() }
            launch { vibrateIcon.activate() }
+36 −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.display.domain.interactor

import android.companion.virtual.VirtualDeviceManager
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.data.repository.fakeDeviceStateRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import org.mockito.kotlin.mock

val Kosmos.connectedDisplayInteractor by
    Kosmos.Fixture {
        ConnectedDisplayInteractorImpl(
            keyguardRepository = fakeKeyguardRepository,
            displayRepository = displayRepository,
            deviceStateRepository = fakeDeviceStateRepository,
            backgroundCoroutineDispatcher = testDispatcher,
            virtualDeviceManager = mock<VirtualDeviceManager>(),
        )
    }
Loading