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

Commit b775c227 authored by Ahmed Mehfooz's avatar Ahmed Mehfooz
Browse files

[SB][ComposeIcons] Add VpnIconViewModel

Test: Manual
Test: VpnIconViewModelTest
Bug: 430344544
Flag: com.android.systemui.status_bar_system_status_icons_in_compose
Change-Id: Ic4c49e7f2cd8b553e4d7cf40cc078c25e2fa291b
parent 8f5b6aa7
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -47,6 +47,8 @@ import com.android.systemui.statusbar.policy.bluetooth.data.repository.bluetooth
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.statusbar.policy.fakeHotspotController
import com.android.systemui.statusbar.policy.fakeNextAlarmController
import com.android.systemui.statusbar.policy.vpn.data.repository.vpnRepository
import com.android.systemui.statusbar.policy.vpn.shared.model.VpnState
import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.statusbar.systemstatusicons.data.repository.statusBarConfigIconSlotNames
import com.android.systemui.testKosmos
@@ -79,6 +81,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
    private lateinit var slotMute: String
    private lateinit var slotNextAlarm: String
    private lateinit var slotVibrate: String
    private lateinit var slotVpn: String
    private lateinit var slotWifi: String
    private lateinit var slotZen: String

@@ -93,6 +96,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
        slotMute = context.getString(com.android.internal.R.string.status_bar_mute)
        slotNextAlarm = context.getString(com.android.internal.R.string.status_bar_alarm_clock)
        slotVibrate = context.getString(com.android.internal.R.string.status_bar_volume)
        slotVpn = context.getString(com.android.internal.R.string.status_bar_vpn)
        slotWifi = context.getString(com.android.internal.R.string.status_bar_wifi)
        slotZen = context.getString(com.android.internal.R.string.status_bar_zen)
    }
@@ -194,6 +198,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
            showEthernet()
            showVibrate()
            showHotspot()
            showVpn()

            assertThat(underTest.activeSlotNames)
                .containsExactly(
@@ -204,6 +209,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
                    slotHotspot,
                    slotNextAlarm,
                    slotVibrate,
                    slotVpn,
                    slotZen,
                )
                .inOrder()
@@ -221,6 +227,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
                    slotHotspot,
                    slotMute,
                    slotNextAlarm,
                    slotVpn,
                    slotWifi,
                    slotZen,
                )
@@ -304,4 +311,8 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
    private fun Kosmos.showHotspot() {
        fakeHotspotController.isHotspotEnabled = true
    }

    private fun Kosmos.showVpn() {
        vpnRepository.vpnState.value = VpnState(isEnabled = true)
    }
}
+139 −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 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.vpn.ui.viewmodel

import android.content.testableContext
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.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.vpn.data.repository.vpnRepository
import com.android.systemui.statusbar.policy.vpn.shared.model.VpnState
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)
@android.platform.test.annotations.EnabledOnRavenwood
class VpnIconViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest =
        kosmos.vpnIconViewModelFactory.create(kosmos.testableContext).apply {
            activateIn(kosmos.testScope)
        }

    @Test
    fun visible_isFalse_byDefault() = kosmos.runTest { assertThat(underTest.visible).isFalse() }

    @Test
    fun visible_vpnIsEnabled_isTrue() =
        kosmos.runTest {
            vpnRepository.vpnState.value = VpnState(isEnabled = true)

            assertThat(underTest.visible).isTrue()
        }

    @Test
    fun visible_vpnStateChanges_flips() =
        kosmos.runTest {
            assertThat(underTest.visible).isFalse()

            vpnRepository.vpnState.value = VpnState(isEnabled = true)
            assertThat(underTest.visible).isTrue()

            vpnRepository.vpnState.value = VpnState(isEnabled = false)
            assertThat(underTest.visible).isFalse()
        }

    @Test
    fun icon_notVisible_isNull() =
        kosmos.runTest {
            vpnRepository.vpnState.value = VpnState(isEnabled = false)
            assertThat(underTest.icon).isNull()
        }

    @Test
    fun notBranded_validated_hasExpectedIcon() =
        kosmos.runTest {
            vpnRepository.vpnState.value =
                VpnState(isEnabled = true, isBranded = false, isValidated = true)
            assertThat(underTest.icon).isEqualTo(EXPECTED_ICON_STANDARD_VALIDATED)
        }

    @Test
    fun notBranded_notValidated_hasExpectedIcon() =
        kosmos.runTest {
            vpnRepository.vpnState.value =
                VpnState(isEnabled = true, isBranded = false, isValidated = false)
            assertThat(underTest.icon).isEqualTo(EXPECTED_ICON_STANDARD_NOT_VALIDATED)
        }

    @Test
    fun branded_validated_hasExpectedIcon() =
        kosmos.runTest {
            vpnRepository.vpnState.value =
                VpnState(isEnabled = true, isBranded = true, isValidated = true)
            assertThat(underTest.icon).isEqualTo(EXPECTED_ICON_BRANDED_VALIDATED)
        }

    @Test
    fun branded_notValidated_hasExpectedIcon() =
        kosmos.runTest {
            vpnRepository.vpnState.value =
                VpnState(isEnabled = true, isBranded = true, isValidated = false)
            assertThat(underTest.icon).isEqualTo(EXPECTED_ICON_BRANDED_NOT_VALIDATED)
        }

    companion object {
        private val BASE_CONTENT_DESCRIPTION =
            ContentDescription.Resource(R.string.accessibility_vpn_on)

        private val EXPECTED_ICON_STANDARD_VALIDATED =
            Icon.Resource(
                res = R.drawable.stat_sys_vpn_ic,
                contentDescription = BASE_CONTENT_DESCRIPTION,
            )

        private val EXPECTED_ICON_STANDARD_NOT_VALIDATED =
            Icon.Resource(
                res = R.drawable.stat_sys_no_internet_vpn_ic,
                contentDescription = BASE_CONTENT_DESCRIPTION,
            )

        private val EXPECTED_ICON_BRANDED_VALIDATED =
            Icon.Resource(
                res = R.drawable.stat_sys_branded_vpn,
                contentDescription = BASE_CONTENT_DESCRIPTION,
            )

        private val EXPECTED_ICON_BRANDED_NOT_VALIDATED =
            Icon.Resource(
                res = R.drawable.stat_sys_no_internet_branded_vpn,
                contentDescription = BASE_CONTENT_DESCRIPTION,
            )
    }
}
+28 −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.vpn.domain.interactor

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.policy.vpn.data.repository.VpnRepository
import com.android.systemui.statusbar.policy.vpn.shared.model.VpnState
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow

@SysUISingleton
class VpnInteractor @Inject constructor(vpnRepository: VpnRepository) {
    val vpnState: StateFlow<VpnState> = vpnRepository.vpnState
}
+5 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.statusbar.systemstatusicons.ethernet.ui.viewmodel.Et
import com.android.systemui.statusbar.systemstatusicons.hotspot.ui.viewmodel.HotspotIconViewModel
import com.android.systemui.statusbar.systemstatusicons.ringer.ui.viewmodel.MuteIconViewModel
import com.android.systemui.statusbar.systemstatusicons.ringer.ui.viewmodel.VibrateIconViewModel
import com.android.systemui.statusbar.systemstatusicons.vpn.ui.viewmodel.VpnIconViewModel
import com.android.systemui.statusbar.systemstatusicons.wifi.ui.viewmodel.WifiIconViewModel
import com.android.systemui.statusbar.systemstatusicons.zenmode.ui.viewmodel.ZenModeIconViewModel
import dagger.assisted.Assisted
@@ -59,6 +60,7 @@ constructor(
    muteIconViewModelFactory: MuteIconViewModel.Factory,
    nextAlarmIconViewModelFactory: NextAlarmIconViewModel.Factory,
    vibrateIconViewModelFactory: VibrateIconViewModel.Factory,
    vpnIconViewModelFactory: VpnIconViewModel.Factory,
    wifiIconViewModelFactory: WifiIconViewModel.Factory,
    zenModeIconViewModelFactory: ZenModeIconViewModel.Factory,
) : ExclusiveActivatable() {
@@ -79,6 +81,7 @@ constructor(
    private val muteIcon by lazy { muteIconViewModelFactory.create(context) }
    private val nextAlarmIcon by lazy { nextAlarmIconViewModelFactory.create(context) }
    private val vibrateIcon by lazy { vibrateIconViewModelFactory.create(context) }
    private val vpnIcon by lazy { vpnIconViewModelFactory.create(context) }
    private val wifiIcon by lazy { wifiIconViewModelFactory.create(context) }
    private val zenModeIcon by lazy { zenModeIconViewModelFactory.create(context) }

@@ -92,6 +95,7 @@ constructor(
            muteIcon,
            nextAlarmIcon,
            vibrateIcon,
            vpnIcon,
            wifiIcon,
            zenModeIcon,
        )
@@ -123,6 +127,7 @@ constructor(
            launch { muteIcon.activate() }
            launch { nextAlarmIcon.activate() }
            launch { vibrateIcon.activate() }
            launch { vpnIcon.activate() }
            launch { wifiIcon.activate() }
            launch { zenModeIcon.activate() }
        }
+87 −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.vpn.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.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.vpn.domain.interactor.VpnInteractor
import com.android.systemui.statusbar.policy.vpn.shared.model.VpnState
import com.android.systemui.statusbar.systemstatusicons.ui.viewmodel.SystemStatusIconViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

class VpnIconViewModel
@AssistedInject
constructor(@Assisted private val context: Context, interactor: VpnInteractor) :
    SystemStatusIconViewModel.Default, ExclusiveActivatable() {
    val hydrator = Hydrator(traceName = "VpnIconViewModel.hydrator")

    private val vpnState by
        hydrator.hydratedStateOf(
            traceName = "SystemStatus.vpnState",
            initialValue = VpnState(),
            source = interactor.vpnState,
        )

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

    override val visible
        get() = vpnState.isEnabled

    override val icon
        get() = vpnState.toUiState()

    private fun VpnState.toUiState(): Icon? {
        if (!isEnabled) {
            return null
        }
        val res =
            if (isBranded) {
                if (isValidated) {
                    R.drawable.stat_sys_branded_vpn
                } else {
                    R.drawable.stat_sys_no_internet_branded_vpn
                }
            } else {
                if (isValidated) {
                    R.drawable.stat_sys_vpn_ic
                } else {
                    R.drawable.stat_sys_no_internet_vpn_ic
                }
            }

        return Icon.Resource(
            res = res,
            contentDescription = ContentDescription.Resource(R.string.accessibility_vpn_on),
        )
    }

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

    @AssistedFactory
    interface Factory {
        fun create(context: Context): VpnIconViewModel
    }
}
Loading