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

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

Merge changes Ic4c49e7f,I7106f330 into main

* changes:
  [SB][ComposeIcons] Add VpnIconViewModel
  [SB][ComposeIcons] Add VpnRepository
parents 5cd06b53 b775c227
Loading
Loading
Loading
Loading
+350 −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.data.repository.impl

import android.content.packageManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.connectivityManager
import android.net.vpnManager
import android.os.userManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.net.VpnConfig
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.vpn.data.repository.realVpnRepository
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.Mockito.anyInt
import org.mockito.Mockito.anyString
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@android.platform.test.annotations.EnabledOnRavenwood
class VpnRepositoryImplTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest by lazy { kosmos.realVpnRepository }

    private val mockNetwork: Network = mock()
    private val mockValidatedCapabilities: NetworkCapabilities =
        mock<NetworkCapabilities>().apply {
            whenever(hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).thenReturn(true)
        }
    private val mockNonValidatedCapabilities: NetworkCapabilities =
        mock<NetworkCapabilities>().apply {
            whenever(hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).thenReturn(false)
        }
    private val mockLinkProperties: android.net.LinkProperties =
        mock<android.net.LinkProperties>().apply {
            whenever(interfaceName).thenReturn("test_interface")
        }

    @Before
    fun setUp() {
        whenever(kosmos.userManager.users).thenReturn(listOf(primaryUser))
        whenever(kosmos.userManager.getProfileIdsWithDisabled(primaryUser.id))
            .thenReturn(intArrayOf(primaryUser.id))
        whenever(kosmos.userManager.getEnabledProfileIds(primaryUser.id))
            .thenReturn(intArrayOf(primaryUser.id))

        whenever(kosmos.connectivityManager.allNetworks).thenReturn(arrayOf(mockNetwork))
        whenever(kosmos.connectivityManager.getLinkProperties(mockNetwork))
            .thenReturn(mockLinkProperties)
        whenever(kosmos.connectivityManager.getNetworkCapabilities(mockNetwork))
            .thenReturn(mockNonValidatedCapabilities)

        whenever(kosmos.packageManager.getApplicationInfo(anyString(), anyInt()))
            .thenReturn(nonBrandedAppInfo)
    }

    @Test
    fun isVpnEnabled_whenVpnConnected_isTrue() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupVpnConfig(primaryUser.id, testVpnConfig)

            assertThat(vpnState?.isEnabled).isFalse()

            getVpnChangeCallback().onAvailable(mockNetwork)

            assertThat(vpnState?.isEnabled).isTrue()
        }

    @Test
    fun isVpnEnabled_whenVpnDisconnected_isFalse() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupVpnConfig(primaryUser.id, testVpnConfig)
            getVpnChangeCallback().onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isEnabled).isTrue()

            whenever(kosmos.vpnManager.getVpnConfig(anyInt())).thenReturn(null)
            getVpnChangeCallback().onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isEnabled).isFalse()
        }

    @Test
    fun isVpnBranded_whenVpnIsBrandedSystemApp_isTrue() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupVpnConfig(primaryUser.id, testVpnConfig)
            setupAppInfo(brandedSystemAppInfo)

            getVpnChangeCallback().onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isBranded).isTrue()
        }

    @Test
    fun isVpnBranded_whenVpnIsBrandedNonSystemApp_isFalse() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupVpnConfig(primaryUser.id, testVpnConfig)
            setupAppInfo(brandedNonSystemAppInfo) // Use non-system app

            getVpnChangeCallback().onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isBranded).isFalse()
        }

    @Test
    fun isVpnBranded_whenVpnIsNotBranded_isFalse() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupVpnConfig(primaryUser.id, testVpnConfig)
            setupAppInfo(nonBrandedAppInfo)

            getVpnChangeCallback().onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isBranded).isFalse()
        }

    @Test
    fun isVpnValidated_whenVpnIsValidated_isTrue() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupVpnConfig(primaryUser.id, testVpnConfig)

            val callback = getVpnChangeCallback()
            callback.onLinkPropertiesChanged(mockNetwork, mockLinkProperties)
            callback.onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isValidated).isTrue()
        }

    @Test
    fun isVpnValidated_whenVpnIsNotValidated_isFalse() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupVpnConfig(primaryUser.id, testVpnConfig)

            val callback = getVpnChangeCallback()
            callback.onLinkPropertiesChanged(mockNetwork, mockLinkProperties)
            callback.onCapabilitiesChanged(mockNetwork, mockNonValidatedCapabilities)

            assertThat(vpnState?.isValidated).isFalse()
        }

    @Test
    fun isVpnEnabled_whenVpnEnabledForOtherUser_isFalse() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)
            assertThat(vpnState?.isEnabled).isFalse()

            setupMultipleUsers()
            setupVpnConfig(primaryUser.id, null)
            setupVpnConfig(secondaryUser.id, testVpnConfig)

            getVpnChangeCallback().onAvailable(mockNetwork)

            assertThat(vpnState?.isEnabled).isFalse()
        }

    @Test
    fun isVpnBranded_whenBrandedVpnOnOtherUser_isFalse() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupMultipleUsers()
            setupVpnConfig(primaryUser.id, null)
            setupVpnConfig(secondaryUser.id, testVpnConfig.apply { user = secondaryUser.name })
            setupAppInfo(brandedAppInfo)

            getVpnChangeCallback().onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isBranded).isFalse()
        }

    @Test
    fun isVpnValidated_whenVpnOnOtherUserIsValidated_isTrue() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupMultipleUsers()
            whenever(kosmos.userManager.getEnabledProfileIds(primaryUser.id))
                .thenReturn(intArrayOf(primaryUser.id, secondaryUser.id))

            setupVpnConfig(primaryUser.id, null)
            setupVpnConfig(secondaryUser.id, testVpnConfig.apply { user = secondaryUser.name })

            val callback = getVpnChangeCallback()
            callback.onLinkPropertiesChanged(mockNetwork, mockLinkProperties)
            callback.onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isValidated).isTrue()
        }

    @Test
    fun isVpnValidated_whenVpnOnOtherUserIsNotValidated_isFalse() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupMultipleUsers()
            whenever(kosmos.userManager.getEnabledProfileIds(primaryUser.id))
                .thenReturn(intArrayOf(primaryUser.id, secondaryUser.id))

            setupVpnConfig(primaryUser.id, null)
            setupVpnConfig(secondaryUser.id, testVpnConfig.apply { user = secondaryUser.name })

            val callback = getVpnChangeCallback()
            callback.onLinkPropertiesChanged(mockNetwork, mockLinkProperties)
            callback.onCapabilitiesChanged(mockNetwork, mockNonValidatedCapabilities)

            assertThat(vpnState?.isValidated).isFalse()
        }

    @Test
    fun isVpnValidated_whenNoEnabledProfiles_isTrue() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            setupMultipleUsers()
            whenever(kosmos.userManager.getEnabledProfileIds(primaryUser.id))
                .thenReturn(intArrayOf())

            setupVpnConfig(primaryUser.id, null)
            setupVpnConfig(secondaryUser.id, testVpnConfig.apply { user = secondaryUser.name })

            val callback = getVpnChangeCallback()
            callback.onLinkPropertiesChanged(mockNetwork, mockLinkProperties)
            callback.onCapabilitiesChanged(mockNetwork, mockNonValidatedCapabilities)

            assertThat(vpnState?.isValidated).isTrue()
        }

    @Test
    fun vpnState_combinesAllStates() =
        kosmos.runTest {
            val vpnState by collectLastValue(underTest.vpnState)

            assertThat(vpnState?.isEnabled).isFalse()
            assertThat(vpnState?.isBranded).isFalse()
            assertThat(vpnState?.isValidated).isFalse()

            setupVpnConfig(primaryUser.id, testVpnConfig)
            setupAppInfo(brandedSystemAppInfo)

            val callback = getVpnChangeCallback()
            callback.onAvailable(mockNetwork)
            callback.onLinkPropertiesChanged(mockNetwork, mockLinkProperties)
            callback.onCapabilitiesChanged(mockNetwork, mockValidatedCapabilities)

            assertThat(vpnState?.isEnabled).isTrue()
            assertThat(vpnState?.isBranded).isTrue()
            assertThat(vpnState?.isValidated).isTrue()
        }

    private fun setupVpnConfig(userId: Int, config: VpnConfig?) {
        whenever(kosmos.vpnManager.getVpnConfig(userId)).thenReturn(config)
    }

    private fun setupAppInfo(appInfo: android.content.pm.ApplicationInfo) {
        whenever(kosmos.packageManager.getApplicationInfo(anyString(), anyInt()))
            .thenReturn(appInfo)
    }

    private fun setupMultipleUsers() {
        whenever(kosmos.userManager.users).thenReturn(listOf(primaryUser, secondaryUser))
        whenever(kosmos.userManager.getProfileIdsWithDisabled(primaryUser.id))
            .thenReturn(intArrayOf(primaryUser.id))
        whenever(kosmos.userManager.getEnabledProfileIds(primaryUser.id))
            .thenReturn(intArrayOf(primaryUser.id))
    }

    private fun getVpnChangeCallback(): ConnectivityManager.NetworkCallback {
        val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
        val requestCaptor = argumentCaptor<NetworkRequest>()
        verify(kosmos.connectivityManager, atLeastOnce())
            .registerNetworkCallback(requestCaptor.capture(), callbackCaptor.capture())
        return callbackCaptor.lastValue
    }

    companion object {
        private const val IS_BRANDED_KEY = "com.android.systemui.IS_BRANDED"
        private val primaryUser = android.content.pm.UserInfo(0, "primary", 0)
        private val secondaryUser = android.content.pm.UserInfo(10, "secondary", 0)

        private val brandedAppInfo =
            android.content.pm.ApplicationInfo().apply {
                metaData = android.os.Bundle().apply { putBoolean(IS_BRANDED_KEY, true) }
            }

        private val nonBrandedAppInfo =
            android.content.pm.ApplicationInfo().apply {
                metaData = android.os.Bundle().apply { putBoolean(IS_BRANDED_KEY, false) }
            }

        private val brandedSystemAppInfo =
            android.content.pm.ApplicationInfo().apply {
                flags = android.content.pm.ApplicationInfo.FLAG_SYSTEM
                metaData = android.os.Bundle().apply { putBoolean(IS_BRANDED_KEY, true) }
            }

        private val brandedNonSystemAppInfo =
            android.content.pm.ApplicationInfo().apply {
                flags = 0 // Not a system app
                metaData = android.os.Bundle().apply { putBoolean(IS_BRANDED_KEY, true) }
            }

        private val testVpnConfig =
            VpnConfig().apply {
                user = "testuser"
                interfaze = "test_interface"
            }
    }
}
+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,
            )
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -80,6 +80,7 @@ import android.media.session.MediaSessionManager;
import android.nearby.NearbyManager;
import android.net.ConnectivityManager;
import android.net.NetworkScoreManager;
import android.net.VpnManager;
import android.net.wifi.WifiManager;
import android.os.BatteryStats;
import android.os.IDeviceIdleController;
@@ -218,6 +219,12 @@ public class FrameworkServicesModule {
        return context.getSystemService(ConnectivityManager.class);
    }

    @Provides
    @Singleton
    static VpnManager provideVpnManager(Context context) {
        return context.getSystemService(VpnManager.class);
    }

    @Provides
    @Singleton
    static ContentResolver provideContentResolver(Context context) {
+13 −2
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.graphics.drawable.Drawable;

import com.android.systemui.Dumpable;
import com.android.systemui.statusbar.policy.SecurityController.SecurityControllerCallback;
import com.android.systemui.statusbar.policy.vpn.data.repository.VpnRepository;
import com.android.systemui.supervision.data.model.SupervisionModel;

public interface SecurityController extends CallbackController<SecurityControllerCallback>,
@@ -48,11 +49,21 @@ public interface SecurityController extends CallbackController<SecurityControlle
    @Deprecated
    int getDeviceOwnerType(ComponentName admin);
    boolean isNetworkLoggingEnabled();
    /** @deprecated Use {@link VpnRepository#getVpnState()} instead. */
    @Deprecated
    boolean isVpnEnabled();
    boolean isVpnRestricted();
    /** Whether the VPN network is validated. */
    /**
     * Whether the VPN network is validated.
     * @deprecated Use {@link VpnRepository#getVpnState()} instead.
     */
    @Deprecated
    boolean isVpnValidated();
    /** Whether the VPN app should use branded VPN iconography.  */
    /**
     * Whether the VPN app should use branded VPN iconography.
     * @deprecated Use {@link VpnRepository#getVpnState()} instead.
     */
    @Deprecated
    boolean isVpnBranded();
    String getPrimaryVpnName();
    String getWorkProfileVpnName();
Loading